Repository navigation
Description
Summary
Seven of the nine bin/*-proxy.mjs chat proxies crash the whole process on a
POST /v1/chat/completions whose body is valid JSON but not an object — the
literal null. Each handler JSON.parses the body inside a try/catch (so
malformed JSON correctly gets a 400), but then dereferences the parsed value
without checking it is an object, so null throws a TypeError outside any
guard and Node exits. One malformed request from any client therefore kills a
long-lived fleet proxy until its supervisor restarts it.
The other two proxies already do this right and are the in-repo pattern to copy:
bin/clef-proxy.mjs:211—if (!body || typeof body !== 'object')→ 400
"Invalid JSON body: expected an object", process stays up.bin/opencode-route-proxy.mjs:709—if (!isPlainObject(body))→ 400
"request body must be a JSON object", process stays up.
Evidence (main c90fcbc7, verified this run)
First unguarded dereference after JSON.parse in each affected handler:
| proxy | first deref |
|---|---|
bin/design-code-proxy.mjs |
dispatchWithFallback reads body.model at :247 (called from :236) |
bin/codex-proxy.mjs |
Boolean(body.stream) at :1088 |
bin/claude-code-proxy.mjs |
Array.isArray(body.messages) at :723 |
bin/agy-proxy.mjs |
Boolean(body.stream) at :847 |
bin/antigravity-ls-proxy.mjs |
Boolean(body.stream) at :770 |
bin/commandcode-proxy.mjs |
Boolean(body.stream) at :903 |
bin/muse-proxy.mjs |
Boolean(body.stream) at :1601 |
Probe (laptop, real modules, files md5-identical to main; probe dir
~/git/fleet/probe-2026-10-30-laneB-nullbody/): each proxy was spawned on a
scratch port, given a malformed-JSON control ({bad json → HTTP 400, process
alive in all nine), then POST /v1/chat/completions with body null:
- All seven above: fetch failed with
UND_ERR_SOCKET, process exited with
code 1 (uncaughtTypeErrorstack naming the deref line above). - Controls: clef-proxy (
POST /v1/systemone) and opencode-route-proxy both
answered HTTP 400 and stayed alive.
Secondary (code-read, not probed): non-object scalars/arrays take inconsistent
paths instead of a uniform 400 — e.g. an array body reaches design-code's
dispatch with body.model === undefined, and resolveModelConfig
(bin/design-code-proxy.mjs, requestedModel || 'design-code-pro') silently
substitutes the default model for a body that was never a chat request.
Why it matters
These proxies are long-lived fleet infrastructure (supervised, shared by many
sessions). A single stray null body — a buggy client, a health-check tool, a
hand-rolled curl — takes the proxy down for every routed session, and the
failure mode (process exit, socket reset) points at the proxy rather than at
the bad request, so it is hard to attribute after the fact. The two guarded
siblings prove the intended contract: a non-object body is a client error
(400), never a process exit.
Acceptance criteria
- Every
bin/*-proxy.mjsJSON POST handler (chat completions at minimum;
audit the other JSON POST routes in the same files) rejects a parsed
body that is not a plain object with that proxy's existing 400 error
shape, before any property dereference. -
null, array, string, and number bodies all get the 400; the process
stays alive and answers a subsequent valid request. - Each affected proxy's
bin/*.test.mjsgains a regression test in the
existing harness style: non-object body → 400 and the proxy still
serves afterwards (the clef / opencode-route behaviour is the oracle). - No behaviour change for object bodies, malformed JSON (still 400 from
the parse catch), or the guarded clef / opencode-route proxies.
Activity
dezren39 commentedon Oct 30, 2026
🤖 Agent note · filed
12 remaining items
dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up
Merge pull request #3481 from developing-today/queue/5415-2816-proxy-…
dezren39 commentedon Nov 1, 2026
🤖 Agent note · done
Implemented: inline guard after each chat-completions
JSON.parse(design-code :192, codex :1086, claude-code :731, agy :860, antigravity-ls :763, commandcode :909, muse :1594) — non-plain-object body → that proxy's existing 400 shape, before any deref. Audit: one request-body JSON POST route per file; muse/v1/responsesalready guards viaaliasResponsesRequestBody.Tests (laptop): new per-proxy regression tests 7/7 pass (null/array/string/number → 400, proxy still serves). Full suites: design-code 11/11, claude-code 6/6, agy 12/12, antigravity-ls 13/13, commandcode 3/3, muse 25/25; codex 20 pass / 0 fail in a 20-min run cut by the harness timeout (pre-existing soak duration; details on the PR). Codex review on the PR: completed, no findings.