Skip to content

Commit a0cabd2

Browse files
鲁工鲁工
authored andcommitted
docs: capture the protocol-policing learning note
1 parent 659c360 commit a0cabd2

1 file changed

Lines changed: 47 additions & 0 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Gateway 400 on every real session: don't police the upstream's protocol
2+
3+
## The problem, in one line
4+
5+
ccmr 1.8.3 added per-message schema validation (`role` must be user/assistant), and every
6+
real Claude Code v2.1+ session died locally with `400 messages[1].role must be user or
7+
assistant` — Claude Code sends harness context as `role: "system"` entries in `messages`.
8+
9+
## The approach
10+
11+
1. **Recognize whose error text it is before theorizing.** Grep the error string across the
12+
repo: `messages[1].role must be user or assistant` matched `requestValidationError` in
13+
`src/server.ts`. That instantly names the failing layer: the gateway's own local
14+
validation, not the upstream, not Claude Code.
15+
2. **Capture what the client actually sends — don't guess the payload.** Built a 3-piece
16+
loop in the scratchpad, no real keys, no cost:
17+
- fake Anthropic upstream (canned JSON + SSE responses) on one scratch port,
18+
- a ~30-line logging proxy that dumps every request body to `bodies/NNN-req.json` and
19+
forwards to the gateway,
20+
- the real gateway (`node dist/cli.js start -p <scratch> -c <scratch yaml>`),
21+
then ran the real client headless: `ANTHROPIC_BASE_URL=<proxy> claude -p "hi"` with a
22+
scratch `CLAUDE_CONFIG_DIR`. One run reproduced the user's exact error string.
23+
3. **Read the captured body.** `messages[1]` was `{role: "system", content: "Available
24+
agent types..."}` on `POST /v1/messages?beta=true`. Root cause proven, not inferred.
25+
4. **Minimize to one curl** (`messages: [{role:"user"...},{role:"system"...}]` → 400) —
26+
that shape became the regression test at the HTTP seam.
27+
5. **TDD the fix:** failing test first, then deleted the whole per-message validation loop.
28+
Re-ran the un-minimized loop (real headless `claude -p "hi"`) to confirm green end to end.
29+
30+
## The judgment calls
31+
32+
- **Did NOT whitelist `system` as a third allowed role.** That repeats the same mistake one
33+
protocol revision later. The gateway now validates only what routing depends on (body is
34+
an object, `model` string, `messages` is a non-empty array, `stream` boolean) and forwards
35+
everything else untouched.
36+
- **Did NOT touch the user's live gateway or real keys.** The whole loop ran on scratch
37+
ports with a fake upstream; the real client was the only real component — and it's the
38+
one whose behavior was in question.
39+
- **Did NOT trust memory of "what Claude Code sends".** The `role:"system"` entry is
40+
undocumented client behavior; only a live capture could establish it.
41+
42+
## The reusable rule
43+
44+
A passthrough gateway validates only the fields it routes on; every schema check beyond
45+
that is a bet against the client's future — when a proxy suddenly rejects real traffic,
46+
grep the error text to find whose validation fired, then capture live client traffic
47+
through a logging proxy instead of reasoning about what the client "should" send.

0 commit comments

Comments
 (0)