From b090fd4a05d9537e494061dfb854f3af98559048 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Wed, 12 Aug 2026 22:43:21 +0800 Subject: [PATCH 1/2] fix: surface a clear error on non-JSON API responses instead of crashing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/sdk/js/src/gen/client/client.gen.ts | 20 ++++++++++++++++++- .../sdk/js/src/v2/gen/client/client.gen.ts | 13 +++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/sdk/js/src/gen/client/client.gen.ts b/packages/sdk/js/src/gen/client/client.gen.ts index 34a8d0bece..1d805ca6ca 100644 --- a/packages/sdk/js/src/gen/client/client.gen.ts +++ b/packages/sdk/js/src/gen/client/client.gen.ts @@ -114,10 +114,28 @@ export const createClient = (config: Config = {}): Client => { case "arrayBuffer": case "blob": case "formData": - case "json": case "text": data = await response[parseAs]() break + // altimate_change start — upstream_fix: guard JSON parse against non-JSON (HTML) response bodies + // "json" is split out of the fall-through group above so its parse can be guarded: a 200 + // whose body is an HTML error page from a proxy/gateway/CDN otherwise crashes with a raw + // "JSON Parse error: Unrecognized token '<'". The body is read OUTSIDE the guard so a + // network/body-read failure (socket reset, abort) keeps its own error; only an actual + // JSON syntax failure gets the actionable message (mirrors the v2 client). + case "json": { + const text = await response.text() + try { + data = text ? JSON.parse(text) : {} + } 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.`, + ) + } + break + } + // altimate_change end case "stream": return opts.responseStyle === "data" ? response.body diff --git a/packages/sdk/js/src/v2/gen/client/client.gen.ts b/packages/sdk/js/src/v2/gen/client/client.gen.ts index 627e98ec42..01eb095776 100644 --- a/packages/sdk/js/src/v2/gen/client/client.gen.ts +++ b/packages/sdk/js/src/v2/gen/client/client.gen.ts @@ -169,7 +169,18 @@ export const createClient = (config: Config = {}): Client => { // Some servers return 200 with no Content-Length and empty body. // response.json() would throw; read as text and parse if non-empty. const text = await response.text() - data = text ? JSON.parse(text) : {} + // altimate_change start — upstream_fix: guard JSON parse against non-JSON (HTML) response bodies + // A 200 whose body is an HTML error page from a proxy/gateway/CDN otherwise crashes with a + // raw "JSON Parse error: Unrecognized token '<'". Surface an actionable error instead. + try { + data = text ? JSON.parse(text) : {} + } 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.`, + ) + } + // altimate_change end break } case "stream": From 15a452ae4e87e475e6ca832ececbc9e574d0378d Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 13 Aug 2026 08:36:00 +0800 Subject: [PATCH 2/2] fix: preserve the original parse error as the Error cause MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- packages/sdk/js/src/gen/client/client.gen.ts | 3 ++- packages/sdk/js/src/v2/gen/client/client.gen.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/sdk/js/src/gen/client/client.gen.ts b/packages/sdk/js/src/gen/client/client.gen.ts index 1d805ca6ca..c5b4760b74 100644 --- a/packages/sdk/js/src/gen/client/client.gen.ts +++ b/packages/sdk/js/src/gen/client/client.gen.ts @@ -127,10 +127,11 @@ export const createClient = (config: Config = {}): Client => { const text = await response.text() try { data = text ? JSON.parse(text) : {} - } catch { + } catch (cause) { 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.`, + { cause }, ) } break diff --git a/packages/sdk/js/src/v2/gen/client/client.gen.ts b/packages/sdk/js/src/v2/gen/client/client.gen.ts index 01eb095776..67915f39ac 100644 --- a/packages/sdk/js/src/v2/gen/client/client.gen.ts +++ b/packages/sdk/js/src/v2/gen/client/client.gen.ts @@ -174,10 +174,11 @@ export const createClient = (config: Config = {}): Client => { // raw "JSON Parse error: Unrecognized token '<'". Surface an actionable error instead. try { data = text ? JSON.parse(text) : {} - } catch { + } catch (cause) { 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.`, + { cause }, ) } // altimate_change end