Debug a failed request
An ordered path from "something failed" to "this is why" — the status, the error body, the request id, and the gateway's log.
A request failed and you are reading this during an incident, so this guide is ordered by what to check first rather than by what is most interesting. Each step either identifies the failure or hands you to the next one.
The one thing worth doing before an incident: capture x-request-id from every
response, successful or not. It is Salyro's identifier for that request, it is on
every response, and it is what ties the error your code received to the record of
what happened.
Step 1 — Read the status
The HTTP status decides what kind of problem this is, and therefore who fixes it. Read it before reading anything else:
| Status | The problem is | Do |
|---|---|---|
400 | Your request, as written | Change the request. Never retry it. |
401 | Your Salyro key | Fix the key. Never retry it. |
429 | Your request rate | Back off, then retry. |
5xx | Salyro or the provider | Retry with backoff. Then investigate. |
Two of these are never worth retrying, and retrying them is the most common
mistake made under pressure: a 400 and a 401 are the same failure on the
second attempt, and a retry loop over either turns a configuration error into
traffic.
Step 2 — Read the error body
Every failure carries the same shape:
{
"error": {
"message": "Incorrect API key provided.",
"type": "authentication_error",
"code": "invalid_api_key",
"param": null
}
}
Read it in this order:
type— the family the failure belongs to. This is what you branch on first.code— a stable identifier for the specific condition. This is what you branch on in code. It does not change when the wording does.param— the field at fault, when the failure is about one field. On a rejected parameter this names it, and it is usually the whole answer.message— written for a person. Read it, log it, and do not write logic against it.
Step 3 — Recognise which of four things happened
Almost every failure is one of these, and they need different actions:
The key was not accepted
401, type: "authentication_error". The key was missing, mistyped, revoked, or
belongs to a different gateway than the one you meant. Nothing was sent to a
provider and nothing was spent.
Check the value actually reaching the process rather than the value in your configuration file — the two are the same until an environment does not get redeployed. See Authentication.
The request was rejected as written
400. A parameter Salyro does not recognise, or one the selected model cannot
honour, and param names it.
A model this gateway cannot serve lands here too, with param naming model.
There is no default provider and no substitution, so an id that is not in
GET /v1/models is rejected rather than guessed at — connect the credential, or
name an id from the list.
This strictness is deliberate. Salyro does not drop what it cannot honour,
because a request that asks for structured output and receives a cheerful 200
full of prose fails later, somewhere else, as a parsing error — and by then
nothing points back here. The fix is to change the request, or to send it to a
model that supports what you asked for.
The rate limit was exceeded
429. The key's request rate was exceeded, and this is the one family where
retrying is the correct response — after waiting, and backing off rather than
retrying immediately. An immediate retry is more traffic against the limit you
have just exceeded.
Note what this is not: a limit on rate, not on spend. No request is ever rejected for cost reasons. See API keys.
The provider failed
The provider itself rejected or failed the call — its own credential rejected or out of quota, a model it would not serve, an overload or an outage on its side. Salyro normalises these into the same error shape so that an Anthropic failure and an OpenAI failure reach your code the same way.
Whether to retry depends on what the provider reported: a transient failure is worth another attempt, a rejection of the request as written is not.
Step 4 — Find the request in the gateway's log
If the error body did not settle it, the log has the rest: the model, the
provider, the status, the timing and the token counts for that specific request.
Match it with the x-request-id you captured.
Records are written asynchronously, so a failure from a few seconds ago may take a moment to appear. That delay is a property of how the record is written, not a sign that the request is missing — see Logs & conversations.
What the log is good for that the error body is not:
- Did it reach a provider at all? An authentication failure never does. A request that reached a provider and came back failing is a different investigation.
- Was it slow, or was it rejected? The timing tells you which.
- Is it one request or all of them? A single failure among successes points at the request; a wall of them points at a credential or a provider.
Step 5 — Capture the id next time
If you got this far without an x-request-id, that is the thing to fix before the
next incident. It costs one line, and it turns "a request failed around three
o'clock" into one record:
const response = await fetch('https://api.salyro.com/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SALYRO_API_KEY}`,
'Content-Type': 'application/json',
// Optional: your own id, so your logs and Salyro's can be lined up
'X-Client-Request-Id': traceId,
},
body: JSON.stringify({ model, messages }),
});
if (!response.ok) {
const { error } = await response.json();
logger.error('salyro request failed', {
status: response.status,
requestId: response.headers.get('x-request-id'),
type: error?.type,
code: error?.code,
param: error?.param,
});
}
curl -i https://api.salyro.com/v1/chat/completions \
-H "Authorization: Bearer $SALYRO_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Client-Request-Id: local-check-1" \
-d '{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'
-i prints the response headers, which is where x-request-id is. Log the
status, the request id and the three error fields together, and the next failure
is a lookup rather than an investigation.
