Headers and Error Codes
AISIX returns responses through several caller-facing API formats. A failed chat-completions request, an Anthropic-style messages request, and a passthrough route request do not all use the same error envelope.
This reference helps interpret response headers, retry hints, status codes, and error fields. Start with the request path, then read the matching error format before deciding whether the failure is caller-side, gateway-side, or upstream-provider-side.
Error Response Formats
Use the request path to identify the error envelope that applies.
| Response came from | Error envelope | Read |
|---|---|---|
OpenAI-compatible proxy routes such as /v1/chat/completions, /v1/completions, /v1/embeddings, /v1/responses, audio, images, and rerank | {"error": {...}} | OpenAI-Style Proxy Errors |
Anthropic-style proxy routes such as /v1/messages and /v1/messages/count_tokens | {"type":"error","error": {...}} | Anthropic-Style Proxy Errors |
MCP routes under /mcp and /mcp/{server} | JSON-RPC error envelope | MCP Errors |
A2A calls under /a2a/{agent} | Upstream JSON-RPC response; HTTP errors before forwarding; JSON-RPC error for an upstream dispatch failure | A2A Errors |
A2A agent cards under /a2a/{agent}/.well-known/agent-card.json | Agent-card JSON on success; HTTP error response on gateway or upstream failure | A2A Errors |
| Configured passthrough routes | Forwarded upstream status and body, or {"error": {...}} for AISIX-generated failures | Passthrough Errors |
Proxy Response Headers
Operational headers vary by endpoint. Do not treat every header as universal across every /v1/* route.
| Header | When to use it |
|---|---|
x-aisix-call-id | Appears on chat-completions responses. Use it to correlate one gateway call. |
x-aisix-request-id | Appears on every proxy response. Use it to correlate the response with any access logs and usage events the request produces. Some MCP failures occur before usage accounting; see Usage Events. Accepted on the request by default: send your own ID and AISIX uses that value throughout instead of generating one. Which request headers an ID is read from is configurable, so a deployment can extend or disable this. See Reuse Your Own Request ID. |
x-aisix-served-by | Appears on successful chat-completions routing responses. Use it to identify the target model that served the request. |
x-aisix-cache | Appears on chat requests covered by a cache policy: hit, miss, or bypass. Only Cache-Control: no-cache produces bypass; no-store still performs a lookup and therefore reports hit or miss. Use it to check whether the gateway served the response from cache. |
x-aisix-cache-layer | Appears on cache hits: exact for an identical-request match, semantic for an embedding-similarity match. Use it to attribute hits between the two matching layers. |
x-aisix-cache-similarity | Appears on semantic cache hits. The cosine similarity of the matched entry, 0–1. Use it to calibrate a policy's similarity threshold. |
x-ratelimit-* | The per-dimension family, with names ending in -requests, -tokens, and -concurrent. Appears on successful chat-completions responses when the caller API key has rate limits configured. Use it to inspect request, token, and concurrent limit state where applicable. |
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-ResetX-RateLimit-Scope | Appears only on a 429 produced by the AISIX rate limiter itself. Use it to learn which limit was hit and when to retry. See Rate-Limit Rejection Headers. |
Retry-After | Appears on rate-limit, AISIX Cloud budget, and all-candidates-unavailable rejections when the gateway has a retry hint. Also appears on upstream 429 responses when AISIX can parse the upstream retry hint. Use it to tell callers when to retry. |
Rate-Limit Rejection Headers
When the AISIX rate limiter itself refuses a request, the 429 describes the limit that refused it, in both the OpenAI-style and Anthropic-style error envelopes.
| Header | Value |
|---|---|
X-RateLimit-Limit | The configured cap of the limit that refused the request. |
X-RateLimit-Remaining | Headroom left under that cap. 0 on a rejection. |
X-RateLimit-Reset | Seconds until that limit accepts requests again. |
Retry-After | The same number of seconds, so a client can back off on whichever header it already supports. |
X-RateLimit-Scope | An AISIX extension naming the limit that refused: rps, rpm, rph, rpd, tpm, tpd, or concurrency. |
X-RateLimit-Reset is a duration in seconds, not a timestamp, so a client does not need a clock synchronized with the gateway. For a windowed limit it counts down toward the end of the current fixed window. It is never 0; the smallest value is 1.
A caller API key, a model, and a rate-limit policy can each carry several limits at once. Exactly one of them refuses a given request — the first one AISIX finds exhausted — and the headers describe that one. X-RateLimit-Scope is what makes the numbers unambiguous, because the units differ: x-ratelimit-limit: 1000 is 1000 requests under rpm, 1000 tokens under tpm, and 1000 in-flight requests under concurrency.
Two cases do not follow that reading:
- A routing model or semantic router. Dispatch skips each target that is over its own limit and tries the next one; when none is left, the headers describe the last target that refused, which is a limit configured on that target rather than on the alias the request addressed. Read them as "this is why the request could not be placed", not as the caller's own quota.
- An ensemble model. A panel member or judge that exceeds its own limit fails the request with a
429that carries none of these headers, and noRetry-Aftereither.
A concurrency rejection is the one case with no fixed window. A slot frees when some in-flight request finishes, which the gateway cannot predict, so X-RateLimit-Reset and Retry-After both report a fixed 60.
A raw rejection looks like this:
HTTP/1.1 429 Too Many Requests
content-type: application/json
retry-after: 43
x-ratelimit-limit: 100
x-ratelimit-remaining: 0
x-ratelimit-reset: 43
x-ratelimit-scope: rpm
x-aisix-request-id: 018f3c1f-...
{"error":{"message":"request limit exceeded (requests)","type":"rate_limit_exceeded"}}
HTTP header names are case-insensitive, and AISIX writes them in lowercase on the wire. A client reading X-RateLimit-Limit and a client reading x-ratelimit-limit both match.
The X-RateLimit-* headers appear only when AISIX itself refused the request. They are absent from:
- Successful responses. A
200carries the per-dimensionx-ratelimit-*family instead:x-ratelimit-limit-requests,x-ratelimit-limit-tokens,x-ratelimit-limit-concurrent, and theirremainingandresetcounterparts. That family reports the caller API key's ownrpm,tpm, andconcurrencystate only — not a model limit, not a rate-limit policy, and not the per-second, per-hour, or per-day windows. - Upstream
429responses. AISIX forwards the provider'sRetry-Afterwhen it can parse one, but the provider's quota state is not something AISIX knows, so it does not report one of its own. - AISIX Cloud budget rejections. A budget caps spend in currency, not requests or tokens. It shares the
429status and carries structured budget fields inside the error body, plusRetry-Afterwhen the control plane supplied a reset time.
Their presence is therefore the signal that the AISIX limiter itself rejected the request. Retry-After alone is not that signal: as the two cases above show, it appears on other rejections too.
Proxy Status Codes
Use the error type first when the envelope includes one. The status code gives the broad category, and the error type usually identifies the more precise gateway condition.
| Status | Meaning |
|---|---|
400 | The request is invalid. |
401 | Caller authentication is missing or invalid. |
403 | The caller is authenticated but is not permitted by the configured access controls. |
404 | The requested resource was not found. Gateway-generated examples include an unknown model alias, MCP server, or A2A agent. An upstream service can also return 404; AISIX preserves upstream 4xx statuses. |
413 | The request body exceeds the configured proxy request body limit. |
422 | Content was blocked by a guardrail, either by a policy match or because a fail-closed guardrail could not evaluate it. See Guardrail Refusals. |
429 | The request hit a rate limit or AISIX Cloud budget rejection. |
501 | The resolved provider adapter does not implement the requested endpoint. |
502 | The upstream provider returned a server-side failure or the provider adapter mapped an upstream failure into the proxy error format. |
503 | An authentication dependency or provider adapter is unavailable, or every routing candidate was filtered out by runtime status. |
504 | The upstream request timed out. |
OpenAI-Style Proxy Errors
AISIX OpenAI-compatible proxy errors use this envelope:
{
"error": {
"message": "...",
"type": "invalid_request_error"
}
}
The param and code fields are omitted when AISIX has no value for them. AISIX Cloud budget denials include structured budget fields inside the error object, such as scope, limit_usd, spent_usd, period, and retry_after_seconds.
Common AISIX error.type values are:
| Error type | Typical status | Meaning |
|---|---|---|
invalid_api_key | 401 | Caller authentication is missing or invalid. This type also covers an invalid, expired, or unmapped JWT; inspect error.code for the specific condition. |
permission_denied | 403 | The caller API key cannot use the requested model, the caller's client IP is outside the model's allowed_cidrs, or a verified JWT does not satisfy required scopes or claims. |
model_not_found | 404 | The requested model alias is not configured. |
invalid_request_error | 400 or 413 | The request body or endpoint usage is invalid. Oversized OpenAI-style requests return this error type with status 413. |
provider_unavailable | 503 | The selected upstream provider adapter cannot complete the request. |
all_candidates_unavailable | 503 | Every routing candidate was filtered out or unavailable. |
api_error | 503 | AISIX could not complete an internal dependency operation, such as fetching signing keys for JWT authentication. |
content_filter | 422 | The request or response was blocked by a guardrail. error.code is guardrail_unavailable when the guardrail could not evaluate the content, and absent on a policy match. See Guardrail Refusals. |
billing_error | 429 | AISIX Cloud budget enforcement rejected the request because a blocking budget was exceeded or its outage policy denied the request. |
rate_limit_exceeded | 429 | The request exceeded a configured rate limit. |
not_implemented | 501 | The resolved provider adapter does not implement the requested endpoint. |
timeout | 504 | The upstream request timed out. |
upstream_error | Varies, often 502 for upstream server-side failures | The upstream provider returned an error that AISIX rendered through the proxy error format. |
Authentication Error Codes
OpenAI-style proxy errors can include these stable error.code values for caller authentication. Anthropic-style proxy errors omit code; use their HTTP status and status-mapped error.type instead.
These are not the only error.code values. A guardrail refusal carries guardrail_unavailable, a source-IP rejection carries ip_restricted, and a budget denial carries budget_exceeded. See Guardrail Refusals.
error.code | Status | Meaning |
|---|---|---|
api_key_expired | 401 | The caller API key's expiration deadline has passed. |
api_key_disabled | 401 | The caller API key is administratively disabled. |
jwt_invalid | 401 | The JWT is malformed or fails issuer, signature, signing algorithm, audience, required-claim, or not-before validation. |
jwt_expired | 401 | The JWT's exp deadline has passed. |
jwt_claims_rejected | 403 | The JWT is valid but does not satisfy the provider's required scopes or bound claims. |
jwt_identity_unmapped | 401 | The configured identity claim is missing or does not map to a caller API key bound to that OIDC provider. |
jwks_unavailable | 503 | AISIX could not resolve or fetch the provider's signing keys. Check the OIDC discovery or JWKS endpoint, then retry. |
For JWT trust-provider configuration and rejection behavior, see JWT Authentication.
Upstream Provider Errors
OpenAI-style routes render upstream provider failures through the same error envelope, but AISIX does not always return the upstream response unchanged.
Upstream 4xx responses keep the client-visible HTTP class. Native OpenAI upstream errors can keep their OpenAI-style fields. Cross-provider upstream errors use upstream_error and can include a more specific error.code, such as a rate-limit, permission, or model-not-found code.
Upstream 5xx responses generally return 502. AISIX does not expose upstream 5xx response bodies because they can contain provider account, infrastructure, or private diagnostic detail.
Anthropic-Style Proxy Errors
POST /v1/messages and POST /v1/messages/count_tokens use the Anthropic-style error envelope:
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "..."
}
}
The Anthropic envelope omits the param and code fields that the OpenAI envelope can carry. A guardrail refusal on these routes therefore reaches the caller without the guardrail_unavailable code an OpenAI-style route would carry; the failure tag is still named in the message. See Guardrail Refusals.
The nested error.type follows Anthropic SDK-compatible status mappings:
| Status | Anthropic error.type |
|---|---|
400 or 422 | invalid_request_error |
401 | authentication_error |
403 | permission_error |
404 | not_found_error |
408 | timeout_error |
413 | request_too_large |
429 | rate_limit_error |
503 | overloaded_error |
| Other status codes | api_error |
AISIX keeps the 408 mapping for Anthropic SDK compatibility. Gateway-originated timeouts usually surface through provider-error handling rather than as a native 408 response.
See Anthropic-Style Messages API for examples.
MCP Errors
ANY /mcp and ANY /mcp/{server} use MCP Streamable HTTP and JSON-RPC response shapes. Authentication and request-size failures can still use HTTP status codes such as 401 or 413 before the MCP handler runs.
When a guardrail blocks a tool call or tool result, AISIX returns HTTP 200 with a tool result marked isError. MCP reserves JSON-RPC protocol errors for a request that was not valid; a policy rejection is a tool-execution error, so the calling agent reads it as tool output and can adapt:
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"content": [{ "type": "text", "text": "tool call blocked by content policy (guardrail 'block-secrets')" }],
"isError": true
}
}
This differs from OpenAI-compatible routes, where guardrail blocks use HTTP 422 with an OpenAI-style error envelope. For the message wording and the failure-tag vocabulary, see Guardrail Refusals.
A rejected or unknown tool is a protocol error instead — HTTP 200 with error.code -32602 — because the request itself named something the caller may not call.
A2A Errors
POST /a2a/{agent} returns a successful upstream JSON-RPC response unchanged. After the gateway has identified the agent and entered A2A dispatch, an upstream connection failure or non-success response returns HTTP 502 with a JSON-RPC error envelope:
{
"jsonrpc": "2.0",
"id": 42,
"error": {
"code": -32000,
"message": "..."
}
}
A guardrail refusal uses the same JSON-RPC error envelope with HTTP 422; its code is the JSON-RPC -32000, not a gateway error code. See Guardrail Refusals.
Some failures occur before AISIX forwards the A2A request and therefore are not JSON-RPC errors. Missing or invalid caller authentication returns 401. A caller key without access to the registered agent returns 403, and an unknown or disabled agent returns 404. Rate-limit or AISIX Cloud budget rejections return 429.
Agent-card discovery uses ordinary HTTP status codes rather than a JSON-RPC envelope. Missing or invalid caller authentication returns 401, and an agent access denial returns 403. An unknown or disabled agent returns 404. If the gateway cannot fetch a usable card from the upstream agent, it returns 502. If the gateway cannot determine its own public address from the request, it returns 500 rather than a card that still advertises the upstream agent's address.
A streaming method (message/stream, tasks/resubscribe) follows the same rules only until the stream opens. Once the response has started, the status line is already sent, so a later upstream failure cannot become a 502: the gateway relays a JSON-RPC error envelope as the final event of the stream instead, and records the failure on the usage event. An upstream that answers a streaming call with a plain JSON-RPC response rather than a stream has that response relayed as a single event.
Passthrough Errors
A matched passthrough route forwards the upstream status code and body unchanged when AISIX receives an upstream HTTP response and no gateway policy replaces it.
Failures generated by AISIX use the OpenAI-style proxy error envelope. These failures include caller authentication rejection, a guardrail block, and an upstream transport, timeout, or response-decoding failure. They also include a missing allowed_routes grant, and a source outside the route's source_cidrs (403, with error.code: ip_restricted for the source rejection). A guardrail block is a 422, or a terminal SSE error event on a streamed response. See Guardrail Refusals.
A /passthrough/* path that no explicit route claims follows the ordinary empty-body 404 path. Create a passthrough route to claim the path.
Guardrail Refusals
A guardrail stops traffic for one of two reasons, and the difference matters to a caller.
A policy match means the guardrail read the content and refused it. A fail-closed availability failure means the guardrail could not evaluate the content at all, and its configuration refuses what it cannot check.
Both are guardrail refusals. Both use the requested endpoint's own error convention, and both are recorded as guardrail-blocked.
Two separate things identify the second kind, and they are easy to confuse:
error.code, on the surfaces that carry one at all, is a single constant rather than a per-failure value. On the OpenAI-style envelope it isguardrail_unavailable, present only when the guardrail could not evaluate and absent on a policy match. Two surfaces do not follow that rule: a bridged/v1/responsesstream always sendscontent_filterand/v1/realtimealways sendscontent_filtered, on a policy match and an availability failure alike.- The failure tag —
lakera_timeout,unscannable_body, and so on — is a bounded, code-owned vocabulary that appears only insideerror.message, in parentheses. It is not anerror.codevalue and no envelope field carries it.
The message is built to a fixed shape:
<side> rejected: guardrail '<name>' could not evaluate it (<tag>)
<side> rejected: a guardrail could not evaluate it (<tag>)
The second form is used when no single guardrail can be named. That is the case for every refusal the gateway raises on a chain's behalf rather than on one member's verdict. <side> is request or response, and tool call or tool result on /mcp.
A policy match uses the same shape without the tag:
<side> blocked by content policy (guardrail '<name>')
<side> blocked by content policy
On the OpenAI-style routes, the presence of error.code is itself the signal, so branch on that. On a bridged /v1/responses stream and on /v1/realtime the code is a constant. It cannot separate a policy match from an availability failure, so the tag in the message is the only thing that can. Treat the tag vocabulary as a set of known values rather than as a stable position in a string.
Refusal Envelopes by Surface
Each surface keeps its own protocol's error shape, so guardrail_unavailable does not reach every caller. Anthropic-shaped routes have no code field at all, /mcp answers in-band as a tool result, and /a2a answers in JSON-RPC.
| Surface | HTTP | Body | code |
|---|---|---|---|
OpenAI-compatible routes, non-streaming — including /v1/chat/completions, /v1/embeddings, /v1/audio/transcriptions, /v1/audio/translations, and the Batch and Fine-tuning routes | 422 | {"error":{"message":...,"type":"content_filter","code":"guardrail_unavailable"}} | guardrail_unavailable |
/v1/chat/completions, streaming | 200, then a terminal SSE error event | {"error":{"message":...,"type":"content_filter"}} | none |
/v1/responses, where the upstream serves the Responses API natively | 422 | Same as the non-streaming OpenAI envelope | guardrail_unavailable |
/v1/responses, where AISIX bridges a provider that does not serve the Responses API natively | 200, then a terminal SSE error event | Flat, matching the Responses error event: {"type":"error","code":"content_filter","message":...,"param":null,"sequence_number":N} | content_filter |
/v1/messages and /v1/messages/count_tokens, non-streaming | 422 | {"type":"error","error":{"type":"invalid_request_error","message":...}} | The Anthropic envelope has no code field |
/v1/messages, streaming | 200, then a terminal SSE error event | {"type":"error","error":{"type":"invalid_request_error","message":...}} | The Anthropic envelope has no code field |
| Passthrough routes, non-streaming | 422 | Same as the non-streaming OpenAI envelope | guardrail_unavailable |
| Passthrough routes, streaming | 200, then a terminal SSE error event | {"error":{"type":"content_filter","message":...}} | none |
/mcp and /mcp/{server} | 200 | A tool result flagged isError, with the refusal message as its text. See MCP Errors. | Not applicable |
/a2a/{agent} | 422 | {"jsonrpc":"2.0","id":...,"error":{"code":-32000,"message":...}} | code is the JSON-RPC -32000, not a gateway code |
/v1/realtime | No status — the session is already open | A WebSocket text frame, {"type":"error","error":{"type":"invalid_request_error","code":"content_filtered","message":...}}, followed by a close frame with code 1011 and reason content policy | content_filtered |
Two consequences worth planning for. An SDK on an Anthropic-shaped route cannot branch on error.code, because the envelope has no such field — read the HTTP status and the message instead. And on most streaming surfaces the refusal arrives on a response that already returned 200, so a client that only inspects status codes sees a successful, truncated stream.
Failure Tags Raised by the Gateway
These three come from the proxy itself, on a chain's behalf, where no guardrail produced a verdict to carry a tag. They never name a guardrail.
| Tag | Side | Where it occurs |
|---|---|---|
unscannable_body | Request and response | Some content could not be evaluated as delivered. On the request side, this can be a /v1/messages or /v1/messages/count_tokens body the scanner cannot parse. On the response side, it can be a held-back stream left with nothing scannable, a /mcp tool result that does not parse, or a non-streaming audio transcription or translation response that AISIX cannot decode without replacing invalid byte sequences. See Frames AISIX Cannot Scan and Transcripts AISIX Could Not Decode. |
output_buffer_exceeded | Response only | A streamed response outgrew the hold-back cap before it could be scanned. Occurs on /v1/chat/completions, /v1/messages, /v1/responses, and passthrough routes. Only /v1/chat/completions and passthrough routes honor on_buffer_exceeded here; /v1/messages and /v1/responses always fail closed on overflow. |
mask_writeback_failed | Request and response | A mask verdict could not be spliced back into the body, so the content is refused rather than forwarded unmasked. Occurs on /mcp only, on both tools/call arguments and tool results. |
For an unreadable Anthropic request, MCP tool result, or plain-text audio transcript, unscannable_body is refused only when at least one guardrail in scope reads that side of the exchange and fails closed on it. If all readers on that side fail open, AISIX relays the content and records unscannable_body in guardrail_bypassed_reason; for audio, AISIX still scans the best-effort decoded text, so only the replaced byte sequences escape inspection. A chain that does not read that side neither refuses nor records a bypass. A held-back stream left with nothing scannable is different: once an enforcing output guardrail has withheld the stream, AISIX refuses it regardless of fail_open.
output_buffer_exceeded is usually not a status-code refusal. On most routes the stream has already returned 200, so the refusal arrives as a terminal SSE error event and the held frames are dropped.
The one exception is /v1/responses against a provider that serves the Responses API natively. Nothing has been delivered there, so the request is refused with a 422. When AISIX bridges a provider that does not serve that API, the same overflow arrives as a terminal SSE error event instead.
Failure Tags Raised by a Guardrail Kind
Each remote guardrail kind classifies its own backend failures into a bounded set. The same tag is used whichever way the row is configured. It is the Bypass reason when the guardrail fails open, and the refusal tag when it fails closed, so one outage reads the same either way.
| Guardrail kind | Failure tags |
|---|---|
lakera | lakera_timeout, lakera_throttled, lakera_5xx, lakera_too_large, lakera_config_error |
presidio | presidio_timeout, presidio_throttled, presidio_5xx, presidio_too_large, presidio_config_error |
openai_moderation | openai_moderation_timeout, openai_moderation_throttled, openai_moderation_5xx, openai_moderation_too_large, openai_moderation_config_error |
azure_content_safety and azure_content_safety_text_moderation | azure_cs_timeout, azure_cs_throttled, azure_cs_5xx, azure_cs_config_error |
bedrock | bedrock_timeout, bedrock_throttled, bedrock_too_large, bedrock_5xx |
aliyun_text_moderation and aliyun_ai_guardrail | aliyun_timeout, aliyun_throttled, aliyun_5xx, aliyun_bad_response, aliyun_config_error |
custom | custom_timeout, custom_script_error, custom_engine_error, custom_no_verdict, custom_bad_verdict, custom_unknown_action |
semantic | semantic_embed_unresolved, semantic_embed_timeout, semantic_embed_upstream |
The two Azure kinds share one vocabulary because they call the same service, and so do the two Alibaba Cloud kinds. Read a tag as naming the backend, not the guardrail row — use the guardrail name in the message for that.
The tags mean what their names suggest. _timeout is the configured wall clock elapsing, and _throttled a 429 from the backend. _5xx is a server-side or transport failure. _config_error is a non-429 4xx, such as a rejected credential or a wrong endpoint. _too_large is a payload the backend refused for its size.
aliyun_bad_response is a 2xx whose body was not the documented shape. It is kept separate from aliyun_5xx because the two want different fixes.
The custom_* set distinguishes a script that timed out, threw, or could not be started. It also covers a script that returned nothing, returned a shape that is not a verdict, or returned an action outside the vocabulary.
The built-in keyword and pii guardrails run inside the gateway and call no backend, so they have no failure tags. A block from either carries no error.code at all.
Where Else You Meet These Tags
A guardrail kind's own tags appear on four operator-facing surfaces, so a tag read from a caller's error message can be searched for directly:
- Usage record audit hits. A fail-closed refusal is recorded with
actionblocked_unavailableand the tag inerror_type. An ordinary policy match recordsactionblockedand leaveserror_typeempty. A monitor-mode observation that could not evaluate records the tag on itswould_blocksummary. - The
aisix_guardrail_latency_secondsPrometheus histogram. Itserror_typelabel carries the tag for a fail-closedblockedresult, abypassedresult, and a failed monitor-modewould_block. Ordinary policy matches usenone. Theresultlabel staysblockedfor a fail-closed refusal rather than splitting into a value of its own, so an existingresult="blocked"alert keeps counting it. See Metrics Reference. - The
aisix_guardrail_bypasses_totalPrometheus counter. A fail-open execution increments the counter under the same tag in itsreasonlabel. It counts bypass events rather than requests and has no guardrail, kind, or phase labels. guardrail_bypassed_reasonon the usage event. When a guardrail fails open, nothing is refused and no message reaches the caller, but the same tag is recorded here. Every proxied route's usage event carries this field; retroactive batch-attribution events are the exception because no guardrail chain was resolved for them.
The three tags the gateway raises itself are the exception. They are not guardrail executions, so they do not appear in an audit hit or in the guardrail execution histogram's error_type. A fail-closed refusal records guardrail_blocked and names the tag in the caller's message and gateway log. When an unscannable_body is released by the applicable fail-open policy, the usage event records that tag in guardrail_bypassed_reason. The aisix_guardrail_bypasses_total counter still increments, but no latency-histogram observation is emitted because no guardrail executed.