Skip to content

fix: surface a clear error on non-JSON API responses instead of crashing - #1093

Open
ralphstodomingo wants to merge 2 commits into
mainfrom
fix/sdk-client-non-json-response
Open

fix: surface a clear error on non-JSON API responses instead of crashing#1093
ralphstodomingo wants to merge 2 commits into
mainfrom
fix/sdk-client-non-json-response

Conversation

@ralphstodomingo

@ralphstodomingo ralphstodomingo commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What

When a proxy / gateway / CDN returns an HTTP 200 with an HTML body (an error or interstitial page) instead of JSON, the generated SDK client crashes with a raw JSON Parse error: Unrecognized token '<'. parseAs falls back to "json" (?? "json") whenever Content-Type is missing or unrecognized, so a non-JSON body reaches the parser. The error response path was already guarded; the success path was not.

Fix

Guard the JSON parse in the success path of both generated clients. On a parse failure, throw an actionable error naming the received content-type + HTTP status ("…usually a proxy or gateway error page, not the API") instead of the raw parse crash.

  • packages/sdk/js/src/v2/gen/client/client.gen.ts — the client the CLI imports (@opencode-ai/sdk/v2)
  • packages/sdk/js/src/gen/client/client.gen.ts — v1: json is split out of the shared fall-through group so arrayBuffer/blob/formData/text keep dispatching via response[parseAs]()

Both hunks are wrapped in altimate_change start — upstream_fix: markers — the repo convention for local deviations from upstream, so the bridge-merge process sees and carries them, and they can be retired if/when the fix lands upstream.

Verification (E2E under Bun, full parse-mode matrix)

Drove each actual client file against a local server. 7 cases × v1/v2 × before/after:

Case main (before) this PR
json + HTML body (JSON content-type) SyntaxError: JSON Parse error: Unrecognized token '<' (v2) / Failed to parse JSON (v1) Expected a JSON response but received application/json (HTTP 200). This is usually a proxy or gateway error page, not the API.
json + valid JSON parsed parsed (unchanged)
json + empty body {} {} (unchanged)
parseAs: blob Blob Blob (unchanged)
parseAs: arrayBuffer ArrayBuffer ArrayBuffer (unchanged)
parseAs: text exact string exact string (unchanged)
parseAs: formData (real multipart) FormData field=value FormData field=value (unchanged)
interrupted body mid-read (raw-TCP reset) socket error propagates socket error propagates (unchanged — body read kept outside the guard, per Codex review)

The v2 "before" error is the exact string seen in telemetry (JavaScriptCore phrasing → confirms the crash runs in the Bun CLI, not the Node extension). packages/sdk/js typecheck (tsgo --noEmit) passes.

Where it came from

Surfaced by the extension telemetry-triage bot as a recurring ChatPanel:chat:sendMessageError (~11 machines / 7d).

🤖 Generated with Claude Code

https://claude.ai/code/session_01LKJeLDMhBaYu16LrjGCf25

@ralphstodomingo ralphstodomingo self-assigned this Aug 12, 2026
@github-actions

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (2)
  • packages/sdk/js/src/gen/client/client.gen.ts is excluded by !**/gen/**
  • packages/sdk/js/src/v2/gen/client/client.gen.ts is excluded by !**/gen/**

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 71f0b82e-3d93-42a0-8857-921b85f2c3b1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

Verified via E2E repro (Bun)

Drove the actual @opencode-ai/sdk/v2 client under Bun against a mock server returning a 200 with an HTML body (a proxy/gateway error page that keeps a JSON content-type):

Before — unpatched client on main:

RESULT: THREW  name=SyntaxError  message=JSON Parse error: Unrecognized token '<'

The exact string from telemetry. The JavaScriptCore phrasing confirms it runs in the Bun CLI (not the Node extension), and the throw pins the crash to the JSON success-path parse in client.gen.ts.

After — this PR:

RESULT: THREW  name=Error  message=Expected a JSON response but received application/json (HTTP 200). This is usually a proxy or gateway error page, not the API.

Control — valid JSON 200 against the patched client: parses fine ({"ok":true,"hello":"world"}), no regression on the happy path.

@ralphstodomingo
ralphstodomingo force-pushed the fix/sdk-client-non-json-response branch 2 times, most recently from 1c24cce to 8249569 Compare August 12, 2026 17:01
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

Re-verified after the review round: both hunks now wrapped in altimate_change start — upstream_fix: markers (the repo's bridge-merge convention for local deviations), v1's json case split out so arrayBuffer/blob/formData/text keep their native dispatch, and the full parse-mode matrix re-run E2E under Bun on both clients, before and after — 28/28 as expected (see updated PR description). packages/sdk/js typecheck passes.

@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 82495695bb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

case "json":
try {
data = await response.json()
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve body-read failures outside the JSON parse guard

When a response stream fails after the headers arrive—for example, because the transfer is interrupted or the request is aborted—response.json() rejects with that network/body-read error before parsing JSON. This broad catch replaces it with a misleading proxy/non-JSON message, losing the error needed for diagnosis or retry classification. Keep body consumption outside the parse guard as the v2 implementation does, or only translate actual JSON syntax errors.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed empirically and fixed in b090fd4a0 — reproduced the exact failure with a raw-TCP mid-body reset (Content-Length promised, socket terminated after a partial body): the previous v1 guard swallowed the socket error and mislabeled it as the proxy/gateway message; v2 was unaffected. v1 now mirrors v2 (body read outside the guard, only JSON.parse inside): the reset case propagates The socket connection was closed unexpectedly, the HTML-body case keeps the actionable error, and the full parse-mode matrix + typecheck still pass.

When a proxy, gateway or CDN returns an HTTP 200 with an HTML body (an error
or interstitial page) instead of JSON, the generated SDK client JSON-parses it
and throws a raw `JSON Parse error: Unrecognized token '<'`. `parseAs` falls
back to "json" whenever Content-Type is missing or unrecognized, so a non-JSON
body reaches the parser. The error path was already guarded; the success path
was not.

Guard the JSON parse in both the v1 and v2 generated clients: on a parse
failure, throw an actionable error (non-JSON response, likely a proxy/gateway
error page, with HTTP status + content-type) instead of the raw parse crash.
In v1, "json" is split out of the shared fall-through group so the other parse
modes (arrayBuffer/blob/formData/text) keep dispatching via response[parseAs]().

Both hunks are wrapped in `altimate_change start — upstream_fix:` markers, the
repo convention for local deviations that should survive upstream bridge
merges and eventually land upstream.

Surfaced from telemetry as a recurring extension sendMessageError.
@ralphstodomingo
ralphstodomingo force-pushed the fix/sdk-client-non-json-response branch from 8249569 to b090fd4 Compare August 12, 2026 17:19
@ralphstodomingo
ralphstodomingo marked this pull request as ready for review August 13, 2026 00:30
Copilot AI lite review requested due to automatic review settings August 13, 2026 00:30

@claude claude Bot 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.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

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.

Pull request overview

This PR hardens the generated JavaScript SDK clients to handle non-JSON bodies on successful (2xx) responses by converting raw JSON parse crashes into an actionable error that calls out the received Content-Type and HTTP status.

Changes:

  • Add a guarded JSON.parse on the success path for the v2 generated client when parsing JSON from response.text().
  • Split "json" out of the v1 client’s fall-through parse switch so JSON parsing can be guarded without affecting other parseAs modes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/sdk/js/src/v2/gen/client/client.gen.ts Wraps JSON parsing in a try/catch on 2xx responses to replace raw parse crashes with a clearer error.
packages/sdk/js/src/gen/client/client.gen.ts Separates "json" parsing from the generic response[parseAs]() path to guard JSON parse failures while keeping other parse modes unchanged.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +177 to +182
} catch {
throw new Error(
`Expected a JSON response but received ${response.headers.get("content-type") || "an unknown content type"} ` +
`(HTTP ${response.status}). This is usually a proxy or gateway error page, not the API.`,
)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 15a452ae4 — original SyntaxError now attached via new Error(msg, { cause }), matching the SDK's error-interceptor.ts convention. E2E-verified the cause is present ([cause=SyntaxError] on the HTML-body case) and the full parse-mode + interrupted-body matrix still passes on both clients.

Comment on lines +130 to +135
} catch {
throw new Error(
`Expected a JSON response but received ${response.headers.get("content-type") || "an unknown content type"} ` +
`(HTTP ${response.status}). This is usually a proxy or gateway error page, not the API.`,
)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 15a452ae4 — original SyntaxError now attached via new Error(msg, { cause }), matching the SDK's error-interceptor.ts convention. E2E-verified the cause is present ([cause=SyntaxError] on the HTML-body case) and the full parse-mode + interrupted-body matrix still passes on both clients.

@kilo-code-bot

kilo-code-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • packages/sdk/js/src/gen/client/client.gen.ts
  • packages/sdk/js/src/v2/gen/client/client.gen.ts

Notes:

  • Incremental change since b090fd4a0: both clients now capture the caught value (catch (cause)) and attach it via new Error(msg, { cause }), resolving the two prior Copilot findings that the original SyntaxError was discarded. Standard ES2022 Error cause; cause is always defined here since the guarded body is only JSON.parse(text).
  • Body read (await response.text()) remains outside the try in both files, so the earlier Codex concern (network/body-read failures mislabeled as proxy errors) stays fixed — only a true JSON.parse syntax failure is translated. Verified at client.gen.ts:127-136 (v1) and v2/.../client.gen.ts:171-184.
  • v1's case "json": is split out of the fall-through group, preserving response[parseAs]() dispatch for arrayBuffer/blob/formData/text.
  • altimate_change start/end markers correctly wrap all diverging lines in both files; the newly added catch (cause) and { cause } lines sit inside the existing marked block.
  • Byte-identical guard logic across v1/v2 is intentional for independently-regenerated gen/ files; shared extraction would be non-idiomatic.
Previous Review Summary (commit b090fd4)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit b090fd4)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • packages/sdk/js/src/gen/client/client.gen.ts
  • packages/sdk/js/src/v2/gen/client/client.gen.ts

Notes:

  • Both diffs guard the JSON success-path parse so a non-JSON (proxy/gateway/CDN HTML) 200 body throws an actionable error instead of the raw JSON Parse error: Unrecognized token '<'. Correct and well-scoped.
  • The earlier Codex concern (broad catch swallowing body-read/socket errors) is correctly addressed: const text = await response.text() sits outside the try in both files, so only a true JSON.parse syntax failure is translated — network errors keep their own message. Verified in code at client.gen.ts:127-136 (v1) and v2/.../client.gen.ts:171-184.
  • v1's case "json": split out of the fall-through group preserves the response[parseAs]() dispatch for arrayBuffer/blob/formData/text; only JSON parsing changed.
  • altimate_change start — upstream_fix: / end markers wrap all diverging lines in both files with no nesting/misuse — consistent with the repo's fork-merge convention.
  • No incomplete fix: the two other client.gen.ts files are createClient re-export barrels with no parse logic.
  • The byte-identical error string across v1/v2 is intentional for independently-regenerated gen/ files (self-contained markers must survive per-file regen); extracting shared code would be non-idiomatic here.

Reviewed by glm-5.2 · Input: 20.1K · Output: 3.8K · Cached: 191K

Review guidance: REVIEW.md from base branch main

@cubic-dev-ai cubic-dev-ai Bot 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.

No issues found across 2 files

Re-trigger cubic

Addresses Copilot review: the guard's actionable message discarded the
underlying SyntaxError (token/position detail). Attach it via
new Error(msg, { cause }) — the SDK's existing convention
(error-interceptor.ts).
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 15a452ae4e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants