Skip to content

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 #2821

Open

Description

@dezren39

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 deref

Two failure modes, both verified this run:

  1. null body → HTTP 500, not 400. body.model throws TypeError: Cannot read properties of null (reading 'model') at antigravity-direct.mjs:550. The server-level catch (:644+) converts it to a 500 internal_error and logs a stack — a client error reported as a server fault.
  2. Scalar/array bodies (42, "hello", []) → fabricated upstream request, HTTP 200. Nothing dereferences badly, so buildV1Request (:294) runs resolveModelAndEffort(undefined, undefined), producing the model name -medium (empty base + -medium) and contents: [], and the proxy makes a real upstream generate call with that empty conversation, returning a successful chat.completion whose model field claims gemini-3.8-flash — a model the upstream was never asked for. Against the real upstream the -medium call presumably errors there instead, which is still wrong: a malformed client request becomes a wasted upstream round-trip and an upstream_error that 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/completions with 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 — mirroring clef-proxy.mjs:211 / opencode-route-proxy.mjs:709.
  • No upstream request is made for a non-object body (the stub-upstream oracle: zero streamGenerateContent calls for those cases).
  • A new bin/antigravity-direct.test.mjs in the existing bin/*.test.mjs node-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 (model gemini-3.8-flash-medium, contents preserved).
  • Well-formed object bodies and the /v1/models + /healthz routes are byte-for-byte unchanged.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    effort/sSmall: expected under 2 hours of work
    app/antigravity-directbin/antigravity-direct.mjs direct Cloud Code door
    on Oct 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (lane A, new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a, run 2026-10-30 14:35 CDT
    • Found via: filed this run
    • Task: Triage the non-object JSON body boundary in bin/antigravity-direct.mjs (null 500s; scalar/array bodies fabricate an empty '-medium' upstream request and return 200) and pin the 400 guard + regression tests
    • Expectation: effort/s — done when non-object bodies get a 400 before any dereference, no upstream call is made for them, and bin/antigravity-direct.test.mjs pins it; well-formed bodies unchanged
    • Links: probe (laptop) ~/git/fleet/probe-2026-10-30-laneA-antigrav-body/ · related bin proxies: a JSON null body crashes seven chat proxies — parse succeeds, first deref throws, process exits 1 (clef and opencode-route already 400) #2816 (the seven *-proxy.mjs handlers; this door landed after its snapshot and is outside its glob)
  3. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 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).

  4. 9 remaining items

  5. developing-today commented on Nov 3, 2026

    @developing-today
    Contributor

    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

  6. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
  7. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  8. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  9. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  10. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: claimed

    Queue task #7984 is claimed to refresh the prior triage of issue #2821.

  11. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: triaged

    Fresh triage of issue #2821 is complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/antigravity-directbin/antigravity-direct.mjs direct Cloud Code dooreffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions