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.type | error.code and cause | What to do |
|---|---|---|
400 invalid_request_error | content_policy_violation: a policy blocked the request or the buffered response | Show the refusal. Don't retry. |
401 authentication_error | missing_api_key, invalid_api_token, or tokens.expired: the token is missing, wrong, revoked, or expired | Fix the token. Don't retry. |
402 cost_budget_exceeded | tokens.budget_exhausted: the token's budget is used up | Check the budget. Don't retry. |
402 payment_required | usage_authorization_budget_exceeded: on a connected gateway, the request would exceed a budget policy window or the token allowance | Check the budget. Retry-After may be present; when it is, don't retry before that time. |
403 access_denied | tokens.request_limit_exhausted, or a permission denial for the model | Check the token's quota and permissions. Don't retry. |
422 invalid_request_error | session_id_mismatch: the body's session_id differs from the session header | Fix the payload, then retry. |
429 rate_limit_exceeded | consumer_group_rate_limit_exceeded: a consumer group rate limit is active | Honor Retry-After, within a bounded retry policy. |
429 insufficient_quota | usage_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_unavailable | usage_authorization_rate_limited: on a connected gateway, the request would exceed an access policy's per-minute request or token limit for the token | Retry-After may be present; when it is, wait that long. Retry within a bounded retry policy. |
502 server_error | upstream_provider_error or provider_primary_path_error: the provider call failed | Retry 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 incorrectx-request-idheader - 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.
| Status | error.type |
|---|---|
401 | authentication_error |
402 | billing_error |
403 | permission_error |
404 | not_found_error |
409 | conflict_error |
413 | request_too_large |
429 | rate_limit_error |
503 with usage_authorization_rate_limited | rate_limit_error |
504 | timeout_error |
529 | overloaded_error |
Other 5xx | api_error |
Other 4xx, such as 400, 408, and 422 | invalid_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
4xxresponse, except408,409, and429 - A
429withusage_authorization_request_limit_exceeded - A
402withusage_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:
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
- Run
verdictan events tail --since 10m --jsonin one terminal. - Send a request your policies block, and check that the client gets
400withcontent_policy_violation. - Match the response's request ID to the event, using the normalization rules, and check that the event records the block.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| The client library throws one generic exception for a block | It maps every 4xx to one exception class | Read error.type and error.code from the body before choosing what to do. |
| A blocked request appears several times in events | The client retried automatically | Set the client's retry count to zero. |
A stream returns 200, then stops without content | An output control blocked it mid-stream | Read 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 request | The status is 408, 409, 429, or 5xx without x-should-retry: false, or the gateway is older than 0.1.20 | Set a bounded SDK retry count. Upgrade a gateway that is older than 0.1.20. |
| Error text changed after an upgrade | The gateway reworded the message | Branch on error.type and error.code, never on error.message. |
Next steps
- Debugging Requests: match a client error to its event and policy decision.
- Tests for AI Applications: test the block and escalation paths automatically.