A stream aborted by the guardrail buffer cap reports guardrail_blocked with no policy named
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 157
- Forks
- 32
- Avg merge
- 1h 25m
- Merged PRs (30d)
- 145
Description
Found by the independent pre-merge audit of #1028.
The gap
When a streamed response exceeds a guardrail's max_buffer_bytes and the row is configured on_buffer_exceeded: fail_closed, the gateway refuses the request — but no chain member ever returned a verdict, because the abort happens in the relay loop rather than in the fold:
crates/aisix-proxy/src/chat.rs(StreamOutputPolicy::BufferFull, theelsearm) setsguard.comp().guardrail_blocked = truedirectly;crates/aisix-proxy/src/passthrough_route.rsdoes the same withblocked = true;/v1/responseshas the equivalent branch.
record_execution is what writes the audit log, and it only runs inside the chain fold. So the emitted UsageEvent carries guardrail_blocked: true with guardrail_enforced_hits: [].
Why it matters
#1024 exists so an operator can answer "which policy refused this request" from the event alone. On this path the answer is missing — the row says a guardrail refused it and names none. It is the same asymmetry AISIX-Cloud#1365 is about, one branch over: an operator reading the refusal cannot tell whether a policy matched or the response simply outgrew the scan buffer, and those call for opposite responses (tune the policy vs raise the cap).
It is narrower than #1365, though, and not wrong in the same way: the boolean is accurate, the array is merely silent. #1028 documents the state explicitly on GuardrailEnforcedHit's doc comment so a consumer does not read the empty array as "no guardrail acted".
Shape of a fix
The abort knows everything an entry needs — which member's policy forced the hold-back, the hook (output by construction), and that the action is a refusal. Two plausible designs:
- Record it through the existing log: give the relay a handle to the request's
GuardrailAuditLog(the sameArc#1028 already threads for the emit) and write an entry at the abort, with an action distinguishing "refused because the scan could not complete" from a content decision — theblocked_unavailableprecedent from AISIX-Cloud#1365 applies almost verbatim, since this too is a check that did not run. - Synthesize the entry at the emit site from the hold-back policy that was in force.
(1) is closer to how everything else on this path already works. Either way, the entry must name the row that set the cap, not the chain.
Scope
All three surfaces with a BufferFull hold-back — chat, responses, passthrough routes — since a fix on one is invisible on the others (the handler-family rule in CLAUDE.md).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read the BufferFull hold-back branches in crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/passthrough_route.rs, and the equivalent /v1/responses branch, along with CLAUDE.md and #1028. Trace how record_execution and GuardrailAuditLog produce UsageEvent entries. Done means all three surfaces record an entry naming the policy row and distinguishing an unavailable scan refusal from a content decision.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, backend, observability, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100