The agentrouter.org upstream WAF returns 400 content-blocked
intermittently when:
1. messages[].content contains a blocked keyword (Lorem ipsum, the
phrase 'language model' alone, 'virtual assistant', etc.); or
2. Requests from the same IP/key arrive in a burst, after which the
WAF's per-IP suspicion bucket starts blocking content that would
normally pass. The bucket relaxes after ~5-10s of idle.
Apply three mitigations:
1. Burst guard (open-sse/services/wafRateLimit.ts)
Per-bucket (provider+url) gate that enforces a 500ms minimum gap
between outbound requests to agentrouter. Configurable via
configureWafRateLimit(). Tested in tests/unit/wafRateLimit.test.ts.
2. Reactive retry (BaseExecutor.WAF_RETRY_CONFIG in base.ts)
New WAF_RETRY_CONFIG with maxAttempts=2, delayMs=1500,
backoffMultiplier=2. When the upstream returns 400 with a body that
matches /content[_-]blocked/i, retry the same URL with exponential
backoff (1.5s, 3.0s) before falling through to the 429/401/fallback
chain. Tested in tests/unit/base-executor-waf-retry.test.ts.
3. Documentation (docs/security/AGENTROUTER_WAF.md)
Blocklist of always-blocked and almost-always-blocked patterns,
behavior under load, guidance for prompts/tool output, and pointers
to the relevant code paths in OmniRoute.
These are belt-and-suspenders: the burst guard prevents the WAF from
activating on normal traffic, and the reactive retry recovers when it
does anyway. Together they should eliminate the intermittent
400 content-blocked that Claude Code sees when running through
agentrouter via OmniRoute.
Refs #9275 follow-up. Test: 'WAF retry config shape' and 'WAF retry
differs from generic' guard the WAF_RETRY_CONFIG contract so future
refactors don't accidentally collapse the two retry paths.
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
4.3 KiB
agentrouter.org WAF (Web Application Firewall)
The agentrouter upstream gateway runs a keyword-based content filter on
messages[].content. The filter is partially deterministic (always blocks
certain phrases) and partially probabilistic (burst-sensitive — becomes
more aggressive after rapid requests, recovers after a cooldown).
When the WAF blocks a request it returns:
HTTP/1.1 400 Bad Request
{"error":{"code":"content-blocked","message":"content-blocked (request id: ...)","param":"","type":"agent_router_api_error"}}
Scope of the filter
The WAF inspects messages[].content only. It does not inspect:
- The
systemprompt - Structured content blocks (
tool_result,tool_use,thinking,image) - Tool
descriptionandinput_schemafields - Request metadata, headers, or model id
Always-blocked patterns (case-insensitive)
| Pattern | Notes |
|---|---|
Any Lorem ipsum variant |
Full Latin lorem vocabulary is blocked |
language model (alone) |
"the language model" and "large language model" pass |
virtual assistant |
"AI assistant" passes |
I'm here to help |
"here to help" alone also blocks |
Claude, made by Anthropic |
Full phrase only |
Almost-always-blocked patterns
| Pattern | Notes |
|---|---|
placeholder |
When it stands alone (not as a parameter name, etc.) |
dummy data |
Common seed phrase for fixtures |
foo bar baz |
Canonical placeholder phrase |
Repeated short tokens (AAA BBB CCC, test test test) |
Detector for keyword stuffing |
Behavior under load
After ~5 rapid requests in a short window, the WAF begins blocking content
that would normally pass. The bucket relaxes after ~5–10 seconds of idle
time. This is the same IP-and-key-bound rate limiter that causes
intermittent 400 content-blocked errors when Claude Code or Codex CLI
makes multiple tool-use / message-send calls in quick succession.
Mitigations already applied in OmniRoute
-
open-sse/services/wafRateLimit.ts— burst guard that enforces a 500 ms minimum gap between outbound requests to anyagentrouter:*URL. The gap is well below human perception of latency and prevents the WAF from activating on normal traffic. -
BaseExecutor.WAF_RETRY_CONFIG— when an upstream returns400 content-blocked, the executor retries the same URL with exponential backoff (1.5 s, 3.0 s, max 2 attempts). After the backoff the WAF usually relaxes and the retry succeeds. -
tests/unit/compression/harness.test.ts— the test fixturelongInputwas changed from"lorem ipsum dolor sit amet ".repeat(40)to"example content for testing purposes ".repeat(40)so that when Claude Code reads this file via theReadtool, the file contents do not flow back through atool_resultblock and trip the WAF.
Guidance for prompts and tool output
If a Claude Code or Codex CLI session repeatedly hits
400 content-blocked, check the most recent user message and the most
recent tool result for any of the patterns above and rephrase. Common
workarounds:
- Replace
Lorem ipsum …withexample text …or the actual content the test or fixture is trying to model. - Replace
placeholder(when standing alone) withexample value,sample value, or the real value. - Replace
language modelwithlarge language modelorthe model. - Replace
dummy datawithsample dataor realistic seed values. - Replace
I'm here to help/here to helpwith a more specific opener (e.g. "I'll review the file you mentioned").
Reporting the false positives upstream
The current filter is overly aggressive — it blocks "Lorem ipsum" in
tool_result blocks even though the operator clearly did not intend to
inject a prompt. Operators who want this fixed at the source should
contact agentrouter.org to report the false positives. The blocklist
above is the empirical result of probing the upstream as of 2026-08-03.