{"record":{"id":"3b12afe752e38801","repo":"can1357/oh-my-pi","slug":"auth-broker-returned-malformed-json","errorCode":null,"errorMessage":"Auth broker returned malformed JSON","messagePattern":"Auth broker returned malformed JSON","errorType":"exception","errorClass":"AuthBrokerError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/auth-broker/client.ts","lineNumber":427,"sourceCode":"\t): Promise<t> {\n\t\tconst response = await this.#fetchRaw(method, path, opts);\n\t\tconst text = await response.text();\n\t\tconst raw = this.#parseJson(text, response.status);\n\t\tconst validated = RESPONSE_SCHEMAS[opts.schema](raw);\n\t\tif (validated instanceof type.errors) {\n\t\t\tthrow new AuthBrokerError(\"Auth broker response failed schema validation\", {\n\t\t\t\tstatus: response.status,\n\t\t\t\tbody: validated.summary,\n\t\t\t});\n\t\t}\n\t\treturn validated as t;\n\t}\n\n\t#parseJson(text: string, status: number): unknown {\n\t\ttry {\n\t\t\treturn text.length === 0 ? null : JSON.parse(text);\n\t\t} catch (parseError) {\n\t\t\tthrow new AuthBrokerError(\"Auth broker returned malformed JSON\", {\n\t\t\t\tstatus,\n\t\t\t\tbody: text,\n\t\t\t\tcause: parseError,\n\t\t\t});\n\t\t}\n\t}\n\n\tasync #fetchRaw(\n\t\tmethod: \"GET\" | \"POST\" | \"DELETE\",\n\t\tpath: string,\n\t\topts: {\n\t\t\tauth?: boolean;\n\t\t\tbody?: unknown;\n\t\t\tsignal?: AbortSignal;\n\t\t\theaders?: Record<string, string>;\n\t\t\ttimeoutMs?: number;\n\t\t},\n\t): Promise<Response> {","sourceCodeStart":409,"sourceCodeEnd":445,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/auth-broker/client.ts#L409-L445","documentation":"#parseJson attempts JSON.parse on every successful (2xx/304) response body before schema validation (client.ts:423-433). If the body is not valid JSON — including empty-string bodies (which are mapped to null and then typically fail schema validation downstream instead) — the client wraps the SyntaxError as AuthBrokerError \"Auth broker returned malformed JSON\" with the raw text as `body` and the parse error as `cause`. Note this applies to the request path; the SSE stream path has its own separate malformed-JSON error.","triggerScenarios":"Any #request-backed method (healthz, fetchUsage, fetchUsageHistory, reportClientUsage, fetchClientUsageSummary, notifyUsageStale, credential upload/block/disable/refresh/blocks-delete, listDisabledCredentials) returning 2xx with a body that JSON.parse cannot parse: HTML error pages, plain-text messages, truncated JSON, BOM-prefixed bodies, or NDJSON instead of a single JSON document.","commonSituations":"Reverse proxy or captive portal injecting an HTML interstitial/login page on a 200; misconfigured server returning plain text; response truncated by a proxy or connection reset mid-body; gzip/transfer-encoding mishandling producing garbage bytes; a custom broker writing NDJSON or trailing junk after the JSON value.","solutions":["Inspect err.body — it contains the raw text the server sent; if it's HTML you're being intercepted (proxy, captive portal, auth wall).","Check err.cause for the JSON.parse position/message to identify truncation vs. wrong content type.","Fix or bypass the intercepting proxy/network path; ensure the broker URL is directly reachable from the client.","Verify the broker sets Content-Type: application/json and emits exactly one JSON document with no trailing data.","Catch the error and fall back to fetchSnapshot()/retry if the broker is flaky under load."],"exampleFix":"// before\nconst summary = await client.fetchClientUsageSummary();\n\n// after\ntry {\n  const summary = await client.fetchClientUsageSummary();\n} catch (err) {\n  if (err instanceof AuthBrokerError && err.message === \"Auth broker returned malformed JSON\") {\n    logger.error(\"broker returned non-JSON body\", { status: err.status, snippet: err.body?.slice(0, 200) });\n    throw new Error(\"Auth broker response was not JSON — check proxy/network path\");\n  }\n  throw err;\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isMalformedJsonError(err: unknown): err is AuthBrokerError {\n  return err instanceof AuthBrokerError && err.message === \"Auth broker returned malformed JSON\";\n}","tryCatchPattern":"try {\n  result = await client.healthz();\n} catch (err) {\n  if (isMalformedJsonError(err)) {\n    logger.error(\"non-JSON broker response\", { status: err.status, snippet: err.body?.slice(0, 200), cause: err.cause });\n    throw new Error(\"Auth broker returned non-JSON — check for proxy/captive-portal interception\");\n  }\n  throw err;\n}","preventionTips":["Inspect err.body for HTML — its presence means a proxy or portal intercepted the request; bypass or fix the network path.","Reach the broker directly (allowlist its URL) rather than through rewriting proxies or Wi-Fi captive portals.","Ensure the broker always emits Content-Type: application/json with exactly one JSON document, even for errors.","Check err.cause for JSON.parse offsets to detect truncated bodies from flaky connections, and raise proxy buffering limits if needed."],"tags":["json-parse","malformed-response","auth-broker","network"],"backgroundTag":"malformed-json-response","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}