Skip to main content

Troubleshooting

Handle Policy Blocks and Gateway Errors

When a request through the gateway fails, your application has to decide what to do next: fix a credential, show a policy refusal, wait out a rate limit, or report a provider failure. Most failures should not be retried. This page maps each error to the right response, for Chat Completions and Responses. For POST /v1/messages, see Errors on Anthropic Messages.

Before you start​

  • An application that calls the gateway with an OpenAI-compatible client, an Anthropic client, or plain HTTP.
  • A client API token. See Manage API Tokens.
  • Automatic SDK retries turned off while you test error handling. Add bounded retries later, only for failures that are safe to retry.

Read the error envelope​

Errors the gateway generates look like this:

{
  "error": {
    "message": "Request blocked by policy: prompt_injection.detected",
    "type": "invalid_request_error",
    "param": null,
    "code": "content_policy_violation"
  }
}

Branch on error.type and error.code. error.message is for people and logs, and its wording can change between gateway versions. Some provider errors pass through in the provider's own format; see Handle a provider failure. On /v1/messages, the gateway uses the Anthropic error format.

Map each error to an action​

Status and error.typeerror.code and causeWhat to do
400 invalid_request_errorcontent_policy_violation: a policy blocked the request or the buffered responseShow the refusal. Don't retry.
401 authentication_errormissing_api_key, invalid_api_token, or tokens.expired: the token is missing, wrong, revoked, or expiredFix the token. Don't retry.
402 cost_budget_exceededtokens.budget_exhausted: the token's budget is used upCheck the budget. Don't retry.
402 payment_requiredusage_authorization_budget_exceeded: on a connected gateway, the request would exceed a budget policy window or the token allowanceCheck the budget. Retry-After may be present; when it is, don't retry before that time.
403 access_deniedtokens.request_limit_exhausted, or a permission denial for the modelCheck the token's quota and permissions. Don't retry.
422 invalid_request_errorsession_id_mismatch: the body's session_id differs from the session headerFix the payload, then retry.
429 rate_limit_exceededconsumer_group_rate_limit_exceeded: a consumer group rate limit is activeHonor Retry-After, within a bounded retry policy.
429 insufficient_quotausage_authorization_request_limit_exceeded: on a connected gateway, the token has used all of its request limit (max_requests)Ask an administrator to raise or reset the token's request limit. Don't retry. The response has x-should-retry: false.
503 service_unavailableusage_authorization_rate_limited: on a connected gateway, the request would exceed an access policy's per-minute request or token limit for the tokenRetry-After may be present; when it is, wait that long. Retry within a bounded retry policy.
502 server_errorupstream_provider_error or provider_primary_path_error: the provider call failedRetry only if the operation is idempotent and the caller can wait.

Errors on Anthropic Messages​

With gateway 0.1.20 or later, each error that the gateway generates on POST /v1/messages uses the Anthropic error format. Earlier versions use this format only for usage-authorization rejections. The format applies to these errors:

  • Token rejections, such as tokens.request_limit_exhausted, and rejections of an incorrect x-request-id header
  • A body that is not valid JSON (invalid_json), or a body that is larger than 10 MiB (request_too_large)
  • Policy blocks and usage-authorization rejections
  • Provider failures, such as upstream_error, and provider errors in the OpenAI format

The status and the gateway error.code are the same as in the table above. error.type uses an Anthropic error type, and request_id is the x-request-id value:

{
  "type": "error",
  "error": {
    "type": "rate_limit_error",
    "message": "This API key has reached its request limit. Ask an administrator to raise or reset the key's request limit.",
    "code": "usage_authorization_request_limit_exceeded"
  },
  "request_id": "req_..."
}

Other top-level fields, such as the verdictan extension, stay in the body. If an error body already has the Anthropic format or is not JSON, the gateway does not change it.

Statuserror.type
401authentication_error
402billing_error
403permission_error
404not_found_error
409conflict_error
413request_too_large
429rate_limit_error
503 with usage_authorization_rate_limitedrate_limit_error
504timeout_error
529overloaded_error
Other 5xxapi_error
Other 4xx, such as 400, 408, and 422invalid_request_error

Anthropic SDKs obey x-should-retry. Without it, they retry each 408, 409, 429, and 5xx response. The gateway adds x-should-retry: false to these responses:

  • Each 4xx response, except 408, 409, and 429
  • A 429 with usage_authorization_request_limit_exceeded
  • A 402 with usage_authorization_budget_exceeded

Do not retry a response that has x-should-retry: false. A 402 budget rejection can also have Retry-After. When it is present, do not send the request again before that time.

Other 429 responses, such as consumer_group_rate_limit_exceeded, and 5xx responses do not have the header. For these responses, retry within a bounded retry policy, and honor Retry-After when it is present.

A streamed request that fails before the stream starts gets a JSON error response, not an SSE stream. For example, if the provider connection fails, the gateway returns 502 with upstream_error. For a policy block after the stream starts, see Handle a policy block.

Add a policy-block branch​

This function sorts a parsed gateway response into blocked, under review, error, or success, without retrying or logging the whole response:

classify-result.mjs
export function classifyResult(status, headers, body) {
  const code = body?.error?.code;
  if (code === "content_policy_violation") {
    return {kind: "blocked", message: "This request was blocked by policy."};
  }
  if (headers.get("x-verdictan-verdict") === "ESCALATE") {
    return {kind: "review", message: "This request needs human review."};
  }
  if (status < 200 || status >= 300 || body?.error) {
    return {kind: "error", status, code: code ?? "unknown_error"};
  }
  return {kind: "success", body};
}

Parse the body as JSON only when the content type says so. Treat a non-JSON body or a network exception as a transport failure, and use the table above for error results. The function also works on /v1/messages, because the Anthropic error format keeps the gateway code in error.code.

Handle a policy block​

For a non-streaming request, a block is a 400 with the envelope above. It is a deliberate refusal, not a transport failure: show it to the user, log error.code with the request ID, and don't resend the same content.

For a streaming request, the stream can start before an output control blocks it. The status stays 200, and the same error envelope arrives as an SSE data: event. Check each stream event for an error object; the initial status is not enough. Text before the blocked term can arrive before the error. With gateway 0.1.26 or later, no part of the blocked term arrives. See Output blocks in a stream.

On /v1/messages with gateway 0.1.20 or later, a block in a stream arrives as an Anthropic error event. Anthropic SDKs raise an error when they read this event. verdictan.reason_code gives the policy reason:

event: error
data: {"type":"error","error":{"type":"invalid_request_error","message":"Request blocked by policy: ...","code":"content_policy_violation"},"verdictan":{"verdict":"BLOCK","reason_code":"...",...}}

The Chat Completions route keeps the data: event above, then sends data: [DONE]. On /v1/responses, the block arrives as a final event: error frame. Its data has type: "error", code: "content_policy_violation", a sequence_number, and the same error and verdictan objects.

Handle an escalation​

An escalation is not an error. The gateway returns 200 with x-verdictan-verdict: ESCALATE, choices[0].message.content set to null, and finish_reason set to content_filter. The body usually also has verdictan.escalation.status: "queued", but the header is the reliable signal. Treat the null content as "waiting", not as an empty answer.

A reviewer resolves the escalation in the Inbox. That updates the record; it does not resume or replay the original request.

Handle an invalid request​

Validation errors point at a wrong payload shape or option, for example:

{
  "error": {
    "message": "Body session_id must match the configured session header when both are present",
    "type": "invalid_request_error",
    "param": null,
    "code": "session_id_mismatch"
  }
}

Fix the payload, then retry.

Handle a provider failure​

Provider failures come in two shapes. Some request families pass the provider's status and body through unchanged; others return a gateway envelope with error.type: server_error and a code such as upstream_provider_error. Which one you get depends on the request family and where the failure happened; see Runtime Request Families. On /v1/messages, the gateway envelope has the Anthropic error format, with error.type: api_error for a 5xx status.

Retry only when the status allows it, the operation is idempotent, and the workflow can wait. Never turn a failed provider call into a silent success.

Verify​

  1. Run verdictan events tail --since 10m --json in one terminal.
  2. Send a request your policies block, and check that the client gets 400 with content_policy_violation.
  3. Match the response's request ID to the event, using the normalization rules, and check that the event records the block.

Troubleshooting​

SymptomCauseFix
The client library throws one generic exception for a blockIt maps every 4xx to one exception classRead error.type and error.code from the body before choosing what to do.
A blocked request appears several times in eventsThe client retried automaticallySet the client's retry count to zero.
A stream returns 200, then stops without contentAn output control blocked it mid-streamRead the last SSE data: event for an error object. On /v1/responses and /v1/messages, read the event: error frame.
An Anthropic SDK retries a rejected /v1/messages requestThe status is 408, 409, 429, or 5xx without x-should-retry: false, or the gateway is older than 0.1.20Set a bounded SDK retry count. Upgrade a gateway that is older than 0.1.20.
Error text changed after an upgradeThe gateway reworded the messageBranch on error.type and error.code, never on error.message.

Next steps​