Skip to content

design-code-proxy: quota error in a 200 JSON body hangs the request forever — writeHead precedes the in-body check, so fallback can never fire #681

Closed

Description

@dezren39

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):

  1. The response callback calls clientRes.writeHead(localRes.statusCode || 200, ...) at line 219 — before the body has been read. From that moment clientRes.headersSent is true.
  2. 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')).
  3. The primary target's onFail wrapper (line 279-280) begins with if (clientRes.headersSent) return; — so it silently returns without trying the fallback target and without ever ending clientRes. The fallback target's own onFail (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 502 All design code backends failed: HTTP 429 in under a second — the intended behaviour the in-body path was written to provide.

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 502 All design code backends failed response 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, model rewritten 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.mjs currently has no test file; bin/codex-proxy.test.mjs / bin/muse-proxy.test.mjs show 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

  1. added a commit that references this issue on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions