mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 07:42:13 +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>
37 lines
1.8 KiB
TypeScript
37 lines
1.8 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
// #FIX: regression guard for the WAF retry config. The BaseExecutor must
|
|
// expose a WAF_RETRY_CONFIG with sane defaults and a backoff multiplier
|
|
// so the executor can retry 400 content-blocked before falling through to
|
|
// the 429/401/fallback chain.
|
|
|
|
test("WAF_RETRY_CONFIG has expected shape", async () => {
|
|
const { BaseExecutor } = await import("../../open-sse/executors/base.ts");
|
|
const cfg = BaseExecutor.WAF_RETRY_CONFIG;
|
|
assert.equal(typeof cfg.maxAttempts, "number");
|
|
assert.equal(typeof cfg.delayMs, "number");
|
|
assert.equal(typeof cfg.backoffMultiplier, "number");
|
|
assert.ok(cfg.maxAttempts >= 1, "maxAttempts must allow at least 1 retry");
|
|
assert.ok(cfg.maxAttempts <= 5, "maxAttempts must be bounded to avoid runaway loops");
|
|
assert.ok(cfg.delayMs >= 500, "initial delay must be long enough to clear the WAF");
|
|
assert.ok(cfg.backoffMultiplier >= 1);
|
|
|
|
// Derived: the second attempt should wait at least as long as the first
|
|
const secondAttemptWait = cfg.delayMs * cfg.backoffMultiplier;
|
|
assert.ok(
|
|
secondAttemptWait > cfg.delayMs || cfg.backoffMultiplier === 1,
|
|
"backoffMultiplier should produce a longer wait on the second attempt"
|
|
);
|
|
});
|
|
|
|
test("WAF_RETRY_CONFIG differs from generic RETRY_CONFIG (different problem)", async () => {
|
|
const { BaseExecutor } = await import("../../open-sse/executors/base.ts");
|
|
const generic = BaseExecutor.RETRY_CONFIG;
|
|
const waf = BaseExecutor.WAF_RETRY_CONFIG;
|
|
assert.ok(waf !== generic, "WAF retry config should be distinct from generic retry config");
|
|
// The WAF needs a different starting delay (longer) than the generic 429 path
|
|
// because the WAF's per-IP suspicion bucket relaxes more slowly.
|
|
assert.ok(waf.delayMs >= 500, "WAF initial delay should be >= 500ms");
|
|
});
|