Guardrail Behavior
Guardrails add content-policy checks before AISIX forwards supported gateway traffic, after an upstream responds, or at both points. Their shared runtime controls determine which routes and content are checked, whether a match blocks traffic, and what callers and operators observe.
Built-in keyword and PII guardrails evaluate content inside the gateway. Semantic screening calls a configured embedding model, while custom scripts run operator-defined logic. Provider integrations send extracted content to a moderation service and apply its decision. Choosing a Guardrail Provider compares these options.
All categories use the behavior described on this page. The failure settings normally decide what happens when an applicable check cannot complete; some stream and decode paths have stricter or fixed handling.
Guardrail Hook Point
The hook point controls where AISIX runs a guardrail and what happens when it blocks content:
| Hook Point | What AISIX Checks | Effect When Blocked |
|---|---|---|
input | The caller request before AISIX sends it upstream. | The upstream is not called. |
output | Response content before AISIX returns it to the caller. | The caller does not receive the blocked response. |
both | Both request and response content. | AISIX applies the same guardrail on both sides where the route supports it. |
Input guardrails cover the text-bearing parts of standard inference routes. These include chat completions, completions, responses, messages, embeddings, image generation and editing, video generation, audio speech, and rerank requests. AISIX also scans optional audio transcription and translation prompts, Realtime text frames, MCP tools/call arguments, and A2A message text. The same input checks cover Batch and Fine-tuning request bodies and passthrough route bodies.
Output guardrails scan returned text from chat completions, completions, responses, messages, and audio transcription and translation. They also scan Realtime text frames, MCP tools/call results, Batch and Fine-tuning response bodies, and passthrough route bodies. A2A, embeddings, image generation and editing, video generation, audio speech, rerank, and Count Tokens run only the input hook. Realtime binary frames are relayed without a guardrail content check.
On passthrough routes, guardrails attach through the passthrough_route attachment scope, alongside the caller's API key, team, and environment scopes. The scanned text follows the detected body envelope. Requests carrying a recognized LLM envelope (chat, completions, Responses) scan the extracted message or prompt text, falling back to the whole body when the envelope yields none. Opaque traffic scans the entire request and response body as text.
The request is scanned before dispatch. A blocking verdict returns 422 without contacting the upstream. The built-in pii guardrail cannot write a mask back to a provider-native body, so a mask-action-only match is forwarded unchanged. Use a block action when that content must not reach the upstream. Guardrail kinds such as Presidio and Lakera instead return a blocking verdict when a maskable result has no write-back channel. One case overrides all this. A mask hit inside an Anthropic thinking block is forwarded unchanged for every guardrail kind, because the block is signed. See Reasoning and Thinking Content.
A non-streaming response is scanned in full before relay, and a blocking verdict returns 422 with the upstream body withheld. Streaming responses relay incrementally under the chain's streamed-output policy. Hold-back guardrails buffer frames, either windowed or in full, and release them only after the scanned text clears. A blocking verdict ends the stream with a terminal SSE error frame while held frames are dropped. Each surface uses its own protocol's error shape rather than one canonical frame. Chat completions and passthrough routes carry content_filter. /v1/messages uses the nested Anthropic shape, with an Anthropic-legal error type and no code field. /v1/responses splits by upstream. Where the provider serves the Responses API natively, the held-back stream is refused with a 422 rather than a frame. Where AISIX bridges a provider that does not, the refusal is the flat Responses error event. See Guardrail Refusals.
The Files Routes Are Not Screened
Guardrails do not apply to the Files API at all. POST /v1/files, GET /v1/files, GET /v1/files/{id}, DELETE /v1/files/{id}, and GET /v1/files/{id}/content run no input and no output guardrail check, whatever is attached in the request's scope and whatever fail_open says. An uploaded file leaves the gateway exactly as the caller wrote it, and a downloaded one reaches the caller exactly as the provider stored it. A keyword, PII, or remote guardrail that would refuse the same text in a chat message does not see it here.
Telemetry does not report the gap. A Files usage event carries no marker for the missing guardrail pass: the guardrail evidence fields remain empty. Attaching a fail-closed guardrail to the environment therefore produces no signal that these routes are uncovered. An empty guardrail evidence section on a Files event does not confirm guardrail coverage. Where uploaded content must be screened, screen it before it reaches the gateway.
/v1/batches and /v1/fine_tuning/jobs are not affected. Their request bodies are structured JSON the caller submits rather than an uploaded blob, and both those bodies and the responses are still scanned.
Reasoning and Thinking Content
Reasoning is split by direction, and the two halves are not symmetrical. Plan a PII or blocklist policy around the boundary rather than assuming a guardrail covers everything a reasoning-capable model touches.
Reasoning the caller sends is in scope. Replayed reasoning is caller-supplied text entering the model like any other, so it is scanned, and masked at the same slots:
| Route | Scanned and masked |
|---|---|
/v1/responses | A reasoning item's content[].text and its summary[].text |
/v1/chat/completions | An assistant turn's reasoning_content |
/v1/messages | A thinking block's text — scanned, but see the exception below |
Anthropic thinking blocks are never rewritten. A mask-action hit inside a thinking block is forwarded unchanged. This holds for every guardrail kind, not only the built-in pii one, because rewriting the block would invalidate the signature the provider issued for it.
Block-action rules still fire on a thinking block, so use a block action where such content must not reach the upstream. A redacted_thinking block is a separate case. It carries only encrypted provider data and contributes no text to the scan, so no rule can match inside it. It is likewise relayed untouched.
Reasoning the model generates is out of scope. It is neither scanned nor masked on any route — not on /v1/chat/completions, /v1/messages, or /v1/responses, streaming or buffered.
Generated reasoning used to be scanned incidentally on three paths: buffered /v1/messages, /v1/responses, and passthrough routes carrying one of those envelopes. It no longer is. A block rule whose term appears only inside model-generated reasoning, and nowhere in the answer the model returned, blocked those responses before and does not block them now. The visible answer is scanned exactly as before.
If you relied on that, move the rule to the input hook. Otherwise, accept that generated reasoning is outside the output guardrail's scope.
One exception is not worth relying on either. A passthrough route falls back to scanning the whole body as opaque text whenever envelope extraction yields nothing, and that fallback sweeps in reasoning. It fires for traffic AISIX cannot recognize as a known envelope. It also fires for a recognized envelope that yields no text, such as a Responses body whose output[] holds only a reasoning item. That is an over-scan fallback rather than reasoning handling.
encrypted_contentOn /v1/responses, a reasoning item can carry an encrypted_content field. That field holds encrypted provider data. AISIX forwards it verbatim, never scans it, and never rewrites it.
So masking the readable summary does not remove the sensitive text from the encrypted copy the provider receives. The plaintext the caller can see is masked; the provider still gets the item's encrypted payload intact. Treat a reasoning item's encrypted_content as content that leaves your boundary unmasked, and use a block action rather than a mask action where that is unacceptable.
Guardrail Matching
After selecting a hook point, determine where the guardrail applies:
- In AISIX Cloud, create a guardrail and attach it to the environment or to selected model aliases, MCP servers, caller API keys, or teams. Attachments let one definition apply at several scopes with explicit priority.
- For the open-source AISIX gateway, declare the guardrail in
resources.yamland give it aguardrail_attachmentsentry. Both sources scope a guardrail the same way, so a guardrail the file declares but nothing attaches inspects no traffic.
A guardrail's attachments are the whole of its scope. One with no attachment is enforced nowhere, and that is a valid state rather than an error — the model, API key or team it was scoped to may since have been deleted, which removes the attachment naming it. The console shows such a guardrail as Not attached, and the gateway logs a warning naming it once.
Two consequences worth planning for. Deleting the last attachment does not widen the guardrail; it silences it, so use that to take a rule out of service and keep enabled for whether the rule exists at all. And a guardrail created through the Admin API is attached to nothing until you POST an attachment — the console does this for you when you pick a scope on the create form, but a direct API call does not.
When more than one guardrail applies to the same request, AISIX evaluates them as one chain. A blocking verdict from any guardrail blocks the request or response.
A model-alias scope and an MCP-server scope select things a single request never has both of: an MCP tool call resolves no model, and a model request routes to no MCP server. A model-scoped guardrail therefore never inspects MCP tool calls, and an MCP-server-scoped one never inspects model traffic. To guard both, attach the guardrail at the environment scope or add one attachment of each kind.
An A2A call resolves neither a model nor an MCP server. Only environment, caller API key, and team attachments can cover its message text; model and MCP-server attachments cannot.
If the same guardrail is attached through multiple matching scopes configured in AISIX Cloud, AISIX keeps one copy of that guardrail in the request chain. The highest-priority attachment wins. When priorities are equal, the more specific scope wins in this order: caller API key, team, then model alias or MCP server, then environment. Model alias and MCP server share a rank because no single request can match both.
Enforcement Modes
A guardrail's enforcement_mode controls what AISIX does after a guardrail detects matching content.
Choose one of two enforcement modes:
blockrejects matching content. This is the default whenenforcement_modeis omitted.monitorallows matching content and records what the guardrail would have done.
Detection-based guardrails, including PII recognizers, injection classifiers, and category moderation, can produce false positives and false negatives. Monitor mode is useful when you need to tune a policy against real traffic before enforcing it.
In block mode, an input match stops the request before the provider is called, and an output match prevents the caller from receiving the response.
In monitor mode, the caller-visible request and response do not change. A request that block mode would reject with 422 Unprocessable Entity returns the normal upstream result instead. Content that a redacting guardrail would mask also reaches the model or caller unchanged.
Gateways with this behavior record each observation on the request's usage record as a monitor hit. A hit carries the guardrail name, the hook that matched, and what the guardrail would have done: would_block with a code-owned guardrail kind and outcome summary, or would_mask with safe match counts. If evaluation was unavailable, the would_block summary also includes the bounded, code-owned failure tag, such as custom_timeout; it does not include the script's reason or screened content. Custom scripts use the fixed count name custom and the number of text segments that would change; script-provided count names and values are ignored.
AISIX Cloud stores usage record detail verbatim. Records created by older gateways can therefore retain legacy script-provided reasons, count names, and values, including values derived from request content or secrets. Upgrading the gateway protects new records but does not rewrite historical records; keep access to legacy usage data restricted accordingly.
These records let you stage a new guardrail, observe its hit rate, tune the policy, and then switch it to block. For block observations, AISIX also records the match in a gateway log line at info level:
guardrail in monitor mode observed a violation; not blocking (enforcement_mode=monitor)
Streaming Output
When an enforcing guardrail covers the output hook, AISIX holds streamed response content until the relevant check can run. Depending on the guardrail kind, AISIX may hold the full response or inspect buffered windows. This prevents blocked or unmasked content from reaching the caller before the guardrail decision, and it lets AISIX detect spans that cross stream chunks.
A guardrail in monitor mode never causes streamed output to be held back. When every output guardrail runs in monitor mode, AISIX forwards the stream as it arrives. After the stream ends, AISIX runs the checks and records any matches.
The buffer cap below does not apply to a monitor-only output chain. AISIX delivers and observes an oversized response instead of rejecting it. If any enforcing guardrail also covers the output hook, AISIX holds the stream so the enforcing chain can inspect it before delivery.
Guardrail kinds that expose stream buffer settings use these defaults:
{
"max_buffer_bytes": 262144,
"on_buffer_exceeded": "fail_closed"
}
For AISIX Cloud, add these fields inside the guardrail's config object. For the open-source AISIX gateway, add them directly to the guardrail entry in resources.yaml. fail_closed blocks oversized output. fail_open releases output that AISIX could not fully inspect or rewrite, so use it only when availability is more important than strict output enforcement. Guardrail kinds that do not expose these fields use the built-in cap and fail closed on overflow.
Frames AISIX Cannot Scan
Holding a stream back only helps if the guardrail can read what is being held. Everything in this section applies only where an output-hook guardrail is actually holding the stream back; without one, /v1/responses and /v1/messages relay the frames the upstream sent verbatim, and none of it runs.
On those two routes, AISIX reads the buffered response frame by frame, and a frame is readable when its data: payload is a single JSON document, empty, or the [DONE] sentinel. Before the scan runs, AISIX resolves the buffered body on those two routes, so that the block check and the masking pass see exactly the same frames:
- A frame carrying several
data:lines is read as those lines joined with a newline, as the server-sent events specification requires. Such a frame is scanned and masked in full. - A final frame that the upstream left without its terminator is completed and delivered when AISIX can read it: its payload parses, or there is nothing in it to scan — an empty payload, the
[DONE]sentinel, or nodata:line at all. It is then scanned and masked like any other frame. - A terminated frame whose payload is not one JSON document, such as plain text, is removed from the response instead of being released unscanned. Blocking still sees the removed text, so a forbidden phrase inside such a frame still blocks the whole response. Masking cannot be applied to it structurally, which is why the frame is dropped rather than rewritten.
- A final unterminated frame AISIX can read neither way is cut before either check runs. Its text is neither released nor scanned.
A caller sees a removal as a streamed response that is missing frames. AISIX logs a warn line for each response it cuts, naming the hook, how many frames and bytes went, and why, so an operator can tell a truncated response from a short one. Frames with no data: line at all, such as comment and keepalive frames, carry nothing to scan and are left in place; so are empty payloads and the [DONE] sentinel.
AISIX refuses the response outright when it is left with nothing it could scan, rather than answering with an empty 200. That happens when removal leaves nothing to return, and on /v1/messages when a held-back stream accumulates more than 1 MiB without a frame terminator. The refusal carries the failure tag unscannable_body, which reports that the guardrail was never offered any content to scan — not that a policy matched, and not that the hold-back buffer cap was hit, which uses output_buffer_exceeded instead. On /v1/responses it is a 422:
{
"error": {
"message": "response rejected: a guardrail could not evaluate it (unscannable_body)",
"type": "content_filter",
"code": "guardrail_unavailable"
}
}
On /v1/messages the response has already started as a stream, so the same refusal arrives as a terminal SSE error event carrying the same message. That event uses the Anthropic envelope, so it carries invalid_request_error and no code field rather than the content_filter and guardrail_unavailable an OpenAI-style route would return:
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "response rejected: a guardrail could not evaluate it (unscannable_body)"
}
}
In both cases the request is recorded as guardrail-blocked.
These refusals do not consult fail_open. By the time they fire, an output-hook guardrail is already holding bytes back that nothing scanned, so releasing them is not what the setting asks for. See The Streamed-Output Exception.
The test AISIX applies to a frame is whether its payload parses, which is the most this layer can decide without knowing every event type a provider may emit. One shape therefore still reaches the caller unscanned:
- A frame whose payload is valid JSON but of an event type AISIX does not model. It parses, so the seal keeps it. No scan pass and no masking pass recognizes it, so no text is extracted from it and its bytes are relayed unchanged.
This is deliberate. Rejecting it would cut responses that are correct and delivered today. Read a buffered output guardrail as enforcement over the response text AISIX can read, not as a guarantee that every byte released was inspected.
Two neighboring shapes are easy to mistake for the same thing, and neither is.
Chat completions loses such content rather than releasing it. /v1/chat/completions also holds streamed output back and scans it, but it does not resolve frames the way described above, and it never relays upstream bytes verbatim — every chunk the caller receives is rebuilt from a decoded event. A frame its decoder cannot read is therefore not delivered unscanned; it is not delivered at all. The clearest case is an upstream that ignores stream: true and answers with a plain JSON body. Chat completions chooses the streaming path from the request's own stream flag rather than from what the upstream sent, so that body reaches an SSE decoder that finds no data: lines, yields no events, and leaves the caller with none of the content the upstream sent. Where the model carries a streaming read budget the attempt fails over instead, and an exhausted chain answers with an error — either way the content is lost rather than released unscanned. /v1/responses and /v1/messages do not behave this way: they follow what the upstream actually sent, so a JSON document answering stream: true takes the buffered scan and mask instead.
Passthrough routes over-scan rather than under-scan. A frame that does not parse there has its raw payload handed to the scan, so an unreadable frame is scanned too broadly rather than escaping. A passthrough route also buffers and scans a response body that is not labelled as server-sent events. On both paths the guardrail returns a verdict only — a passthrough response is never rewritten, so a mask-action rule can block it or allow it, and an allowed body is relayed unmodified.
When a Guardrail Cannot Complete Its Check
Two different things stop a guardrail from returning a decision, and they reach different sets of kinds.
- A backend that does not answer. A guardrail kind that calls an external service can fail to reach it, time out, be throttled, or have the call rejected. This covers AWS Bedrock Guardrails, Azure AI Content Safety, Azure AI Content Safety text moderation, Alibaba Cloud Content Moderation, Alibaba Cloud AI Guardrails, Lakera Guard, OpenAI Moderation, Presidio, semantic screening (which calls an embedding model), and custom scripts. A custom script can also fail on its own account — it can throw, time out, or return a verdict AISIX cannot read.
- Content the gateway cannot present to the guardrail as delivered. Examples include an Anthropic request body the scanner cannot parse, an MCP tool result that is not the expected JSON response, and a plain-text audio transcript that AISIX cannot decode without replacing invalid byte sequences. AISIX raises this on the chain's behalf, so it reaches every kind. In the audio case, AISIX still scans the best-effort decoded text; see Transcripts AISIX Could Not Decode.
fail_open governs both causes, on every kind. Built-in keyword and PII guardrails never call a backend, so only the second cause can arise for them — but that cause is theirs as much as anyone's, which is why the field is not a no-op for those two.
fail_open Now Reaches keyword and piiEarlier gateways treated fail_open as a no-op for those two kinds. A keyword or pii guardrail carrying fail_open: true — set at a time when the field did nothing — stops refusing bodies AISIX cannot read as soon as the gateway is upgraded, with no operator action and no control-plane change. Review those rows before you upgrade rather than after.
Which Hook Each Setting Governs
| Setting | Check Side | Applies To | Default | When true | When false |
|---|---|---|---|---|---|
fail_open | Input | Every kind | false | Let the request through unscanned. | Block the request. |
output_fail_open | Output | Every kind except keyword and pii | false | Let the response through unscanned. | Block the response. |
fail_open | Output | keyword and pii only | false | Let the response through unscanned. | Block the response. |
A kind that calls out carries its own output_fail_open for the output hook, which leaves fail_open governing the input hook. keyword and pii have no output_fail_open — they never call out — so their single fail_open governs both of their hooks, and sending either kind an output_fail_open is rejected.
Both sides default to blocking: traffic AISIX could not check is not released. Set fail_open to true on a guardrail where availability matters more than enforcement — the bypass is still recorded, so you can alert on it.
In AISIX Cloud, fail_open is a top-level field on the guardrail and appears on the console's create and edit forms for every kind. output_fail_open sits inside the guardrail's config object; the console surfaces it for some kinds, and the Admin API accepts it on every kind that has one. For the open-source AISIX gateway, both sit directly on the guardrail entry in resources.yaml.
fail_open previously defaulted to true. A guardrail that does not set the field now blocks with 422 when its backend cannot be reached, where it previously let the request through unscanned. Guardrails created through AISIX Cloud are unaffected — the control plane has always stored an explicit value for this field. This changes guardrails declared in a resources.yaml that omit it, and guardrails created after upgrading.
The mandatory field is removed. It made a remote guardrail failure block the request even when fail_open was true, and made an unavailable monitor-mode guardrail fatal. Since fail_open now defaults to false, set it explicitly instead — fail_open: false in block mode refuses what the guardrail could not check. A configuration that still carries mandatory is rejected.
What Refuses Content AISIX Could Not Read
The unscannable_body refusals are raised by the gateway on the chain's behalf rather than as any guardrail's verdict, so which rows are in scope is what decides whether one fires at all.
A request, an MCP tool result, or a non-streaming audio transcription or translation response that AISIX cannot decode as delivered is refused only when at least one guardrail in the resolved chain both reads that side of the exchange and is fail-closed on that side. Both halves must hold on the same guardrail. Picture a chain holding one output-only fail-closed guardrail and one input-only fail-open guardrail: the chain reads the request and it contains a fail-closed row, but no single guardrail in it does both, so nothing there justifies refusing the request.
Two ways to fall outside the gate, and both leave the traffic where a deployment with no guardrail leaves it:
- Nothing in scope reads that side. A chain resolved from output-hook attachments alone is never offered the request body, so it cannot be the reason one is refused.
- Every guardrail in scope that reads that side has
fail_open: true. Content the gateway could not scan is a check that did not run, which is exactly whatfail_opengoverns, so the setting releases the traffic instead of refusing it. AISIX records the pass on the usage record as a bypass under the tagunscannable_body.
hook_point defaults to both and fail_open to false, so this narrowing changes what a deployment sees only where one of the two was set explicitly.
Transcripts AISIX Could Not Decode
A non-streaming /v1/audio/transcriptions or /v1/audio/translations response whose body is not valid UTF-8 is one case of that rule. AISIX decodes such a body best-effort so the chain has something to read, replacing each invalid sequence with a replacement character, and would otherwise relay the original bytes to the caller — so the bytes the decode replaced would reach the caller having been read by nothing.
Where the gate holds — a guardrail in scope both reads the response side and is fail-closed on it, both halves on the same guardrail — the response is refused instead. The refusal is a 422 carrying the failure tag unscannable_body, and the request is recorded as guardrail-blocked:
{
"error": {
"message": "response rejected: a guardrail could not evaluate it (unscannable_body)",
"type": "content_filter",
"code": "guardrail_unavailable"
}
}
Where it does not hold, the original bytes are relayed as before, and which of the two ways the chain fell outside the gate decides what is recorded. A chain whose response-side readers are all fail_open: true would have read the transcript and its failure policy let it past, so the pass is recorded on the usage record as a bypass under the tag unscannable_body. A scope holding nothing that reads the response side never offered to screen the transcript at all: it is relayed unscanned with nothing recorded, exactly as on a deployment with no guardrail.
Wherever something in scope reads the response side, the part of the transcript that did decode is scanned regardless of whether the gate refuses. This differs from an unreadable request body, which is not scanned at all. A transcript is prose the caller reads, so only the bytes the decode replaced go unread. That is why a policy match in the readable text still blocks the response on its own terms, and why one request can legitimately carry both guardrail_blocked and a guardrail_bypassed_reason of unscannable_body.
This is about bytes that will not decode, and nothing else. A text, srt, or vtt transcript that is valid UTF-8 is unaffected, and so is any body that parses as JSON — a json or verbose_json transcript is read through the JSON parser, which cannot hand back invalid text. The test is on the bytes the provider returned, not on the response_format the caller asked for: a body that does not parse as JSON falls through to the plain-text path whatever format was requested.
Monitor Mode Does Not Exempt a Row
enforcement_mode: monitor downgrades a guardrail's own verdict. These refusals carry no verdict to downgrade, so monitor mode does not exempt a row from one — a chain of monitor-only guardrails still refuses a request body the scanner could not read, still refuses an MCP tool result that does not parse, and still refuses a non-streaming transcription or translation response it could not decode. fail_open is what governs them, in monitor mode as in block mode.
Backend failures are the other half, and there monitor mode does behave as its name suggests: an unreachable backend is recorded like any other observation and the request proceeds. To make an unreachable backend refuse traffic, use block mode with fail_open: false.
Some refusals cannot arise on a monitor-only chain at all, for a different reason: a monitor-mode guardrail never holds output back and never masks. So output_buffer_exceeded and the held-back stream's unscannable_body have no hold-back to occur in, and mask_writeback_failed has no mask to fail. All three fire, and are not downgraded, as soon as an enforcing guardrail shares the chain. The request-side unscannable_body, the MCP checks, and the non-streaming transcription and translation responses are different — none of them needs a held-back stream or a mask — which is why a monitor-only chain still raises those. See Guardrail Refusals.
The Streamed-Output Exception
The gate above stops at the streamed-output path, deliberately.
Once an output-hook guardrail is already holding a streaming response back, a stream that leaves nothing scannable behind is refused whatever fail_open says. Honoring the setting there would not skip a refusal; it would release already-buffered bytes that nothing ever scanned, which is a different decision. output_buffer_exceeded is exempt for the same reason, and so is mask_writeback_failed — a mask AISIX could not splice back into the body is content it would have to forward unmasked.
This is not a corner case. keyword inherits the default hold-back streaming policy and pii sets its own, so any enforcing output-hook row of either kind takes that path. A monitor row does not: monitor mode never holds output back, so it never reaches this exception.
MCP tool results and non-streaming audio transcripts that are not valid UTF-8 are gated normally. They are not the held-back stream exception, so their checks still follow the applicable output failure policy.
When AISIX allows traffic because a guardrail could not return a decision, the request or response proceeds without complete inspection. The bypass is recorded on the request's usage record as described in Caller Response and Telemetry.
Caller Response and Telemetry
When a guardrail blocks proxy traffic, AISIX preserves the error convention of the requested endpoint:
- OpenAI-compatible routes return
422withcontent_filter. - Passthrough routes return
422with the same OpenAI-stylecontent_filterenvelope. - Anthropic-compatible non-streaming errors return
422withinvalid_request_error. - Anthropic-compatible streaming responses surface the block as an Anthropic SSE error frame, with an Anthropic-legal error type and no
codefield. - Realtime sessions surface the block as a WebSocket error frame followed by a close, since there is no status line left to carry it.
- A2A calls return
422with a JSON-RPC error envelope.
For the exact envelope on each surface, and for the failure-tag vocabulary a fail-closed refusal names in its message, see Guardrail Refusals.
MCP guardrail blocks follow the MCP protocol shape instead. AISIX returns HTTP 200 with a JSON-RPC result flagged isError, not a protocol error, so the calling agent reads the refusal as tool output and can adapt. See Headers and Error Codes.
Guardrail Metrics
AISIX records each timed guardrail execution in the aisix_guardrail_latency_seconds Prometheus histogram. Labels identify the guardrail name, kind, phase, and result, including monitor-mode would_block/would_mask outcomes and fail-open bypassed results. On gateways with this behavior, the error_type label carries the bounded failure cause for a fail-closed blocked, a bypassed, and a failed monitor-mode would_block. Ordinary policy matches use none. The result label stays blocked for a fail-closed refusal rather than taking a value of its own, so filter on error_type to separate an outage from a policy decision. Streaming window mode can record several executions for one output response. Synchronous per-field PII and keyword masking operations are not timed by this histogram. Use it to track per-guardrail latency (P50/P95/P99), compare local and remote detection, and monitor block and bypass rates. See Metrics Reference for labels and PromQL examples.
The Bypass Reason
A guardrail that fails open does not block the request or response. AISIX records why on the request's usage record, in guardrail_bypassed_reason — shown in AISIX Cloud as Bypass reason. Every route's usage event carries the field. The one exception is the retroactive billing rows AISIX writes for work a provider did inside a batch: no request was proxied there, so no guardrail chain was ever resolved and nothing could have been bypassed.
The value is the guardrail kind's own failure tag — lakera_timeout, bedrock_5xx, custom_script_error and the rest — plus one value the gateway raises itself, unscannable_body, for a body it released because it could not scan it. It is the same bounded vocabulary a fail-closed refusal names in its message, so one outage reads the same whichever way the row is configured. Values are clamped to lowercase a–z, digits and underscore, at most 64 characters, and the first bypass in a request wins: a request that bypassed twice reports the earlier tag.
Two things to get right when reading it.
It is not mutually exclusive with guardrail_blocked. A chain can fail open on one member and be refused by another, and an input hook can fail open on a prompt the provider already answered before the output hook refused the response. Both are requests where something really did go unscreened, and the second is the more compliance-relevant of the two. "Reached a provider unscreened" is the two fields read together, never this one alone.
An empty field does not prove every byte was screened. Some paths scan a best-effort decode without consulting any failure policy. They neither refuse nor record an unscannable_body bypass when decoding replaces bytes:
- A Batch or Fine-tuning response body. When an output hook applies, AISIX scans a best-effort decode without treating decode loss as a failure.
- A passthrough route body. An applicable hook scans a best-effort decode without treating decode loss as a failure.
A fail-closed row does not change either case because these sites do not consult the failure policy for decode loss.
One shape that looks like it belongs on that list does not. A multipart prompt part that is not valid UTF-8, on /v1/audio/transcriptions, /v1/audio/translations, or /v1/images/edits, is rejected with 400 before any guardrail runs. This applies regardless of guardrail attachment or fail_open. It is a structural check on the request, not a guardrail decision, so it neither refuses as a guardrail nor records a bypass.
One further shape does consult a policy, just not this one. A held-back stream that outgrows max_buffer_bytes under on_buffer_exceeded: fail_open releases the buffered content unscanned and unmasked, and skips the end-of-stream scan entirely. The operator asked for that trade-off through a different setting, so it is not recorded as a bypass.
The two Prometheus signals cover different events. aisix_guardrail_bypasses_total counts both guardrail executions that ended in a bypass and proxy-raised unscannable_body passes. The result="bypassed" slice of aisix_guardrail_latency_seconds counts only timed guardrail executions. A backend failure therefore appears in both metrics and the usage record, while a proxy-raised pass appears in the bypass counter and usage record but not the latency histogram.
Next Steps
Choose a provider or configure an in-gateway policy next:
- Choosing a Guardrail Provider: compare built-in and remote guardrail options.
- Built-in Keyword Guardrails: create an in-gateway blocklist.
- PII Detection and Redaction: detect, mask, or block sensitive data inside AISIX.