Repository navigation
Description
Finding
bin/design-code-proxy.mjs's local-dispatch fallback can never fire for a quota error reported in the JSON body of an HTTP 200 response, and the client request hangs forever instead.
In tryTarget (bin/design-code-proxy.mjs:213-266 on main 4b85ef69):
- The response callback calls
clientRes.writeHead(localRes.statusCode || 200, ...)at line 219 — before the body has been read. From that momentclientRes.headersSentistrue. - The non-stream body handler (line 254-258) parses the body and, for a quota error, calls
return onFail(new Error('Quota exceeded in JSON body')). - The primary target's
onFailwrapper (line 279-280) begins withif (clientRes.headersSent) return;— so it silently returns without trying the fallback target and without ever endingclientRes. The fallback target's ownonFail(line 284) is guarded the same way, so the hang happens whichever target returns the quota body.
The status-based path works fine because the 429/>=500 check (line 213) runs before writeHead — only the in-body check, which the code explicitly added to catch quota exhaustion, is defeated by the ordering.
Evidence (reproduced on current main 4b85ef6906a25c1a59694c64986cb17828b6c2b6)
Ran the real bin/design-code-proxy.mjs locally with stub HTTP backends on the Antigravity/Codex ports (DESIGN_CODE_PROXY_PORT/ANTIGRAVITY_LS_PROXY_PORT/CODEX_PROXY_PORT pointed at the stubs), model design-code-flash, non-stream:
- Stub primary returns HTTP 200 with
{"error":{"code":429,"message":"quota stub","type":"insufficient_quota"}}:curl --max-time 5→ timed out after 5002 ms with 0 bytes received (curl exit 28); the proxy logged only the primary attempt — no "Falling back" line, the fallback stub was never contacted, and the response was never ended. Without a client-side timeout this hangs indefinitely.
- Control — stub primary returns HTTP 429 status with an error body:
- Proxy logged
returned 429, tried the fallback target, and answered 502All design code backends failed: HTTP 429in under a second — the intended behaviour the in-body path was written to provide.
- Proxy logged
Why it matters
The in-body quota shape is exactly what this branch exists for (backends that report quota exhaustion in a 200 JSON error body). Today, instead of failing over to the fallback model/backend, every such request wedges an open client connection forever — the worst failure mode for a dispatch proxy whose whole job is fallback — and leaks the socket on both sides until the client gives up.
Suggested fix
For the non-stream path, buffer the body and run the quota check before clientRes.writeHead(...); only write the head once the response is known not to be a quota failure (the stream path must keep writing its head up front). Equivalently, restructure so the head is written at the point the body is ended for non-stream responses.
Acceptance criteria
- A non-stream request whose primary backend returns HTTP 200 with
{"error":{"code":429,...}}or{"error":{"type":"insufficient_quota",...}}tries the fallback target instead of hanging; if the fallback succeeds, its (model-rewritten) response is returned; if it also fails, the client gets the existing 502All design code backends failedresponse promptly. - The same holds when the fallback target returns the in-body quota error: prompt 502, no hang.
- Unchanged behaviour: HTTP-status 429/5xx fallback, connection-error fallback, successful non-stream responses (status, headers,
modelrewritten to the design model id), and streaming responses. - Regression tests cover the in-body quota case (primary and fallback) with stub backends —
bin/design-code-proxy.mjscurrently has no test file;bin/codex-proxy.test.mjs/bin/muse-proxy.test.mjsshow the stub-backend pattern to follow.
Cross-repo check: design-code-proxy exists only in this repo (git grep across developing-today/opencode, developing-today/openchamber, dezren39/nix: no copies) — no cross-repo set.
Activity
Merge pull request #682 from developing-today/creator/681-design-quot…