Skip to main content
A request returned HTTP 400 and your agent stalled. Before you change any code, the gateway already told you what happened: the error code names which control fired, and one of two feeds names the exact rule. This page is the lookup-and-trace flow — read the code, open the right feed, find the rule. If you only remember one thing: a 400 from a security control is not a bug in your prompt. It’s a policy doing its job. Your job is to find which policy, then decide whether to fix the call or relax the rule.

1. Why was my llm request blocked? — start with the error code

Every security block on the hosted gateway returns HTTP 400 with a machine-readable code in the OpenAI-shaped error body. That code is the first fork in the road — it tells you which control plane to debug and which feed to open.
{
  "error": {
    "message": "tool \"shell.exec\" blocked by firewall: destructive shell command",
    "code": "firewall_blocked",
    "type": "invalid_request_error"
  }
}

guardrail_blocked

A Guardrail content rule fired on your request input or the model output. Trace it in the Matches feed. See §2.

firewall_blocked

A Firewall rule denied a tool call. Trace it in the Events feed. See §3.

firewall_approval_pending

A tool call is held for human approval — not denied. Resolve it, don’t debug it. See §4.
All three codes are skip-retry: re-running the identical call routes to the same policy and blocks again. Retrying is wasted latency — fix the input or the rule instead. The full code table lives in Error codes.

2. guardrail_blocked — find the rule in Matches

guardrail_blocked means a content policy attached to your key (or your workspace default) ran a block action against the request input or the model’s output. The block message names the guardrail and the rule, and the request costs you no quota — input blocks fire before metering; output blocks refund the pre-consumed quota. Trace it:
1

Open the Matches feed

In the console, go to the Matches tab on the Guardrails page (GET /api/guardrail/match, Member). Every rule that fires lands here — its RuleType, Action, Stage, and a Detail string like pii: email, phone or matched 3 keyword(s).
2

Filter to the block

Filter by action = block and the time of your request. The matching row tells you the rule type (pii, regex, keyword, max_chars, llm_judge, grounding, external) and whether it fired on the input or output stage.
3

See what it actually matched

By default the feed records that a rule fired and its Detail meta-string — not the matched substring. Turn on Log raw content on that guardrail (it’s off by default, the privacy-conservative posture) to capture the offending string for triage. The toggle is non-retroactive.
A block you believe is wrong? Open the match and mark it a false positive (POST /api/guardrail/match/:id/mark-fp, Admin). To prove the fix before shipping it, edit the rule and re-run the sample in the guardrail editor’s Test tab — no upstream call, no quota.

One concrete example

You send a support reply that contains a customer SSN. Your pii-shield guardrail has an entity_actions override that blocks on ssn:
{
  "type": "pii",
  "stage": "input",
  "action": "mask",
  "entities": ["email", "ssn"],
  "entity_actions": { "ssn": "block" }
}
The gateway returns 400 guardrail_blocked. The Matches feed shows RuleType: pii, Action: block, Stage: input, Detail: pii: ssn. The fix is a product decision, not a code change: relax the override to mask (the model never sees the SSN, the call goes through), or keep the block and strip the SSN upstream. See Guardrails for the full rule-type and PII-entity reference.

3. firewall_blocked — find the verdict in Events

firewall_blocked means a Firewall policy denied a tool call. On the inbound surface it surfaces as 400; through the MCP gateway it surfaces as a tool error (firewall deny: <reason>) so the model can react instead of crashing. The error metadata carries the reason code, risk factors, and score. Trace it in the Events feed (GET /api/workspace/firewall/events, Developer+) — the raw record behind every evaluation. Each event carries a verdict and the surface it was seen on:
VerdictWhat it means for your block
denyA rule (or the default_verdict) blocked the call. This is your firewall_blocked.
auditAllowed but logged — including a [shadow] “would deny” if the policy is in shadow mode.
cap_costThe run’s accumulated spend crossed a per-rule cents cap; resolves to a deny.
1

Filter Events to the denial

Filter by verdict=deny, then by tool, run_id, or session_id to isolate the exact call. The event names the matched rule and the surface — inbound, response, mcp, or egress.
2

Read the reason on the matched rule

The reason string (e.g. destructive shell command, egress host not allowed) tells you whether the rule matched on the tool name, an args_match clause, or an egress destination. The verdict glossary and glob & JSONPath reference decode the matching.
3

Confirm with the Test sandbox

Replay the same tool call in the Firewall Test tab (POST /api/workspace/firewall/test, Developer+) to see the verdict, the matched rule, and the reason — nothing dispatched, nothing logged.
A cap_cost deny is not a rule misfire — your agent run hit the spend ceiling you set. Either the run is looping (check the Runs rollup and the anomaly feed for a retry_loop) or the cap is genuinely too low for the task. Raise the cap deliberately, don’t just retry.

4. firewall_approval_pending — it’s held, not denied

firewall_approval_pending is the one 400 you should not treat as a block. A pending_approval verdict held the tool call for a human; the error body carries an approval id. The call hasn’t failed — it’s waiting.
  1. A reviewer resolves it — from the console (Developer+) or via your own HMAC webhook callback (POST /api/v1/firewall/approvals/:id/callback).
  2. Your agent polls GET /api/v1/firewall/approvals/:id (gateway token) on the id from the error.
  3. Once approved, re-submit the original call with the single-use X-OrcaRouter-Firewall-Approval header, and the gateway lets it through that one time.
See Firewall → Human approval for the full HITL loop.

5. Not a security block? Rule out the key first

Not every 400 is a guardrail or firewall verdict. Before you go feed- diving, rule out the key constraints — these reject before any policy runs and don’t carry the security codes above:
The key’s model_limits allow-list doesn’t include the requested model. Requests for a model outside the list are rejected up front. Add the model to the key or call one that’s allowed.
The key has an allow_ips allow-list and the request came from an address outside it. Add the caller’s IP / CIDR or call from an allowed network.
The key’s credit_limit_usd ceiling is exhausted (0 means unlimited). Raise the cap or rotate to a key with headroom.
The gateway hooks (evaluate, MCP dispatch) require a key with is_firewall_gateway=true. A regular relay key gets 403. Mint a firewall-gateway-scoped key for those routes.

6. The two-minute triage flow

Block on prompt or response text

Code is guardrail_blocked → open Matches, filter action=block, read the Stage + Detail. Fix the content or the rule; prove it in the guardrail Test tab.

Block on a tool call

Code is firewall_blocked → open Events, filter verdict=deny, read the surface + reason. Fix the call or the rule; prove it in the firewall Test tab.

Call is held

Code is firewall_approval_pending → poll the approval id and re-submit with the approval header. Nothing to debug.

None of the above

No security code → check the key: model_limits, allow_ips, credit_limit_usd, or 403 for a missing gateway scope.

Error codes

The full code table — every block, hold, and rejection the gateway can return.

Verdict glossary

What each firewall verdict means and when it resolves to a deny.

Glob & JSONPath

Decode the tool_name_glob and args_match that matched your call.

Guardrails vs Firewall

Which plane fired — text screening or action governance.
For the controls themselves, see Guardrails and Firewall; for the threats they stop, start at the threat model.