Skip to content
Salyro
Guide

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:

StatusThe problem isDo
400Your request, as writtenChange the request. Never retry it.
401Your Salyro keyFix the key. Never retry it.
429Your request rateBack off, then retry.
5xxSalyro or the providerRetry 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:

JSON
{
  "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:

TypeScript
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,
  });
}

-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.

What Salyro will not do for you