mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 19:32:20 +03:00
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>
91 lines
4.3 KiB
Markdown
91 lines
4.3 KiB
Markdown
# 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 `system` prompt
|
||
- Structured content blocks (`tool_result`, `tool_use`, `thinking`, `image`)
|
||
- Tool `description` and `input_schema` fields
|
||
- 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
|
||
|
||
1. **`open-sse/services/wafRateLimit.ts`** — burst guard that enforces a
|
||
500 ms minimum gap between outbound requests to any `agentrouter:*`
|
||
URL. The gap is well below human perception of latency and prevents
|
||
the WAF from activating on normal traffic.
|
||
|
||
2. **`BaseExecutor.WAF_RETRY_CONFIG`** — when an upstream returns
|
||
`400 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.
|
||
|
||
3. **`tests/unit/compression/harness.test.ts`** — the test fixture
|
||
`longInput` was 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 the `Read` tool, the file contents
|
||
do not flow back through a `tool_result` block 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 …` with `example text …` or the actual content
|
||
the test or fixture is trying to model.
|
||
- Replace `placeholder` (when standing alone) with `example value`,
|
||
`sample value`, or the real value.
|
||
- Replace `language model` with `large language model` or `the model`.
|
||
- Replace `dummy data` with `sample data` or realistic seed values.
|
||
- Replace `I'm here to help` / `here to help` with 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. |