Repository navigation
Description
antigravity-direct: a non-object JSON body is never a 400 — null 500s at body.model, and scalar/array bodies fabricate an empty upstream request for model -medium and return 200
Repo: developing-today/code · Main: df9381ff · File: bin/antigravity-direct.mjs (landed today in b0058ec7, PR via feat(antigravity-direct))
Summary
handleChatCompletions (bin/antigravity-direct.mjs:546-550) parses the request body in a try/catch — malformed JSON correctly gets a 400 — but then dereferences the parsed value with no object-shape check:
try { body = JSON.parse(await readBody(req) || '{}'); }
catch { return sendJson(res, 400, { error: { message: 'invalid JSON body', ... } }); }
const requestedModel = body.model || 'gemini-3.8-flash'; // :550 — first derefTwo failure modes, both verified this run:
nullbody → HTTP 500, not 400.body.modelthrowsTypeError: Cannot read properties of null (reading 'model')atantigravity-direct.mjs:550. The server-level catch (:644+) converts it to a 500internal_errorand logs a stack — a client error reported as a server fault.- Scalar/array bodies (
42,"hello",[]) → fabricated upstream request, HTTP 200. Nothing dereferences badly, sobuildV1Request(:294) runsresolveModelAndEffort(undefined, undefined), producing the model name-medium(empty base +-medium) andcontents: [], and the proxy makes a real upstream generate call with that empty conversation, returning a successfulchat.completionwhosemodelfield claimsgemini-3.8-flash— a model the upstream was never asked for. Against the real upstream the-mediumcall presumably errors there instead, which is still wrong: a malformed client request becomes a wasted upstream round-trip and anupstream_errorthat blames Google, never a 400 that blames the request.
Evidence (verified this run, real module)
Laptop probe (~/git/fleet/probe-2026-10-30-laneA-antigrav-body/; bin/antigravity-direct.mjs md5 de11cfa24df8200d66fd788422667ef8 identical on sandbox main df9381ff and the laptop). The proxy ran with SECRET_TOOL_PATH pointed at a stub secret-tool emitting a canned far-future credential and ANTIGRAVITY_DIRECT_UPSTREAM pointed at a local stub upstream that records every payload and answers a canned SSE — no real keyring read and no real Google call.
| Request body | Result |
|---|---|
{bad json (control) |
HTTP 400 invalid JSON body, proxy alive |
null |
HTTP 500 {"error":{"message":"Cannot read properties of null (reading 'model')","type":"internal_error"}}; proxy log: handler error: TypeError … at handleChatCompletions (antigravity-direct.mjs:550:31); proxy alive (server catch) |
42 |
HTTP 200 chat completion (stub-ok from the stub upstream); upstream received model: "-medium", contents: [] |
"hello" |
HTTP 200; upstream received the same fabricated -medium / empty-contents payload |
[] |
HTTP 200; upstream received the same fabricated payload |
{"model":"gemini-3.8-flash","messages":[{"role":"user","content":"hi"}]} (control) |
HTTP 200; upstream received model: "gemini-3.8-flash-medium", contents: [{role:"user",parts:[{text:"hi"}]}] — well-formed handling unchanged |
GET /healthz after all cases |
HTTP 200 |
Why it matters
This door was seated in opencode today as the replacement for the agy / antigravity-ls seats — it is about to become a long-lived shared fleet proxy. Its request-boundary contract is currently: the one body shape that should be the cheapest 400 either masquerades as a server fault (500 + stack in the log, misdirecting incident triage) or — worse for scalars/arrays — silently burns a real upstream call on an empty conversation under a nonsense model name and reports success under a model name that was never invoked. The in-repo oracle already exists: bin/clef-proxy.mjs:211 and bin/opencode-route-proxy.mjs:709 reject non-object bodies with a 400 before any dereference (see #2816).
Dedup / relation to #2816
#2816 (filed earlier today) covers the seven bin/*-proxy.mjs handlers present at c90fcbc7, whose failure mode is a process exit 1. This file is not one of them: it is named antigravity-direct.mjs (outside #2816's bin/*-proxy.mjs acceptance glob), it landed in b0058ec7 after #2816's snapshot, and its failure modes differ (no process exit — the server catch turns the null case into a 500; scalars/arrays fabricate a 200 instead of crashing or substituting a default model). Filing separately so the new door is actually covered; if triage prefers, it can be folded into #2816's fix pass as an eighth handler — the guard and tests are the same shape.
Acceptance criteria
-
POST /v1/chat/completionswith a parsed body that is not a plain object (null, array, string, number, boolean) returns HTTP 400 in this proxy's existing error shape ({error:{message,type:"invalid_request_error"}}), before any property dereference — mirroringclef-proxy.mjs:211/opencode-route-proxy.mjs:709. - No upstream request is made for a non-object body (the stub-upstream oracle: zero
streamGenerateContentcalls for those cases). - A new
bin/antigravity-direct.test.mjsin the existingbin/*.test.mjsnode-test style covers: malformed JSON → 400,null/array/string/number bodies → 400 with no upstream call, and the well-formed object control → 200 with the recorded upstream payload unchanged (modelgemini-3.8-flash-medium, contents preserved). - Well-formed object bodies and the
/v1/models+/healthzroutes are byte-for-byte unchanged.
Activity
dezren39 commentedon Oct 30, 2026
🤖 Agent note · filed
dezren39 commentedon Oct 31, 2026
🤖 Design-architect pass 26 (2026-10-31) — agent note: this issue is adopted as part of Phase 1 of design #3068 ("parsed is not validated"). Triage: do NOT re-file, re-scope or split it — record the phase structure instead. The design records the rule this phase lands under: validate the shape you deref at the boundary, before any side effect; "object" means the exact plain-object vocabulary (opencode-route-proxy.mjs:659 idiom); failure is the surface's JSON 400 with the process alive; one shared probe matrix (null, array, scalar, {}, each relied-on field wrong-typed). Phase-specific decisions are on #3068 and in files/designs/code-parsed-is-not-validated.md (goal workspace).
9 remaining items
developing-today commentedon Nov 3, 2026
agent-note:v1
state: triaged (coverage pass)
Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 40 (priority/normal). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
Found via: coverage pass 2026-11-03
dezren39 commentedon Nov 3, 2026
🤖 Agent note · handed off
dezren39 commentedon Nov 3, 2026
🤖 Agent note · handed off
dezren39 commentedon Nov 4, 2026
🤖 Agent note · handed off
dezren39 commentedon Nov 4, 2026
agent-note:v1
state: claimed
Queue task #7984 is claimed to refresh the prior triage of issue #2821.
dezren39 commentedon Nov 4, 2026
agent-note:v1
state: triaged
Fresh triage of issue #2821 is complete.