Skip to content

fix: reject non-object JSON bodies with 400; keep 502 detail out of responses - #6

Merged
dengyie merged 1 commit into
mainfrom
fix/request-body-validation
Oct 4, 2026
Merged

dengyie merged 1 commit into
mainfrom
fix/request-body-validation

Conversation

@dengyie

@dengyie dengyie commented Oct 4, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to the production code review of PR #5. Two findings, one root cause, fixed at the single parse point.

Findings (both reproduced before the fix)

  1. null request body → response never written, client hangs. JSON.parse('null') returns null legally, but readJsonBody used null as its "error already sent" sentinel and handleChatCompletions trusted it — a literal null body fell through the guard and the handler returned without ever writing a response. Client waits until its own timeout.
  2. 123 / "str" bodies → misleading 502 with leaked internals. These reach forward() and throw TypeError: Cannot use 'in' operator to search for 'reasoning_effort'... inside the strip helper; the upstream catch classifies it as a connect failure (502, counted on the errors dashboard) and embeds the internal exception text in the client-facing message.

Root fix

readJsonBody is the only place any route parses a request body — it now rejects valid-JSON-but-non-object payloads (null/number/string/array) with 400, emitting the correct envelope per route (OpenAI invalid_request_error vs Anthropic error.type). Covers all three routes (/v1/chat/completions, /v1/messages, /v1/messages/count_tokens) with no per-route changes. The 502 message is now a constant 'upstream connect failed'; exception details stay in structured logs.

Tests

  • malformed-body matrix (null, 123, "str", [1,2]) → 400 with parseable response on both chat (OpenAI envelope) and messages (Anthropic envelope) routes — the parseable-response assertion is the hang regression tripwire
  • new mock-reset upstream (socket destroy) → 502 with the exact fixed message, proving no internal detail leaks on the non-timeout fetch-failure path
  • streaming variant added to the reasoning-toggle strip assertions (the real CPA channel traffic is streaming)
  • Full suite: 67/67 passing

…502 detail

Review-reproduced on 2026-10-05: JSON.parse('null') parses legally, but
handleChatCompletions treated a null parsed body as 'error response already
written' and returned without writing anything — the client hung until its own
timeout. Bodies of 123/'str' hit the typeof check inside
stripReasoningTriggers ('in' on a primitive), throwing a TypeError that the
upstream catch block classified as a connect failure: a misleading 502 that
counted toward the errors dashboard and leaked the internal exception text.

Root fix: readJsonBody — the single place every route parses a body — now
rejects non-object JSON (null / number / string / array) with 400, covering
chat completions, messages, and count_tokens with the correct error envelope
per route. The 502 message is fixed to 'upstream connect failed' with details
kept in logs only.

Tests: malformed-body matrix 400 regression (OpenAI + Anthropic envelopes),
upstream connection reset → 502 with the fixed message, and a streaming-path
variant for the reasoning-toggle strip assertions. 67/67 green.
Copilot AI lite review requested due to automatic review settings October 4, 2026 20:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@dengyie
dengyie merged commit ce3a94f into main Oct 4, 2026
2 checks passed
@dengyie
dengyie deleted the fix/request-body-validation branch October 4, 2026 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants