{"record":{"id":"4057dee0445e859e","repo":"earendil-works/pi","slug":"proxy-error-response-status-response-statust","errorCode":null,"errorMessage":"Proxy error: ${response.status} ${response.statusText}","messagePattern":"Proxy error: (.+?) (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/proxy.ts","lineNumber":178,"sourceCode":"\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\tmodel,\n\t\t\t\t\tcontext,\n\t\t\t\t\toptions: buildProxyRequestOptions(options),\n\t\t\t\t}),\n\t\t\t\tsignal: options.signal,\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\tlet errorMessage = `Proxy error: ${response.status} ${response.statusText}`;\n\t\t\t\ttry {\n\t\t\t\t\tconst errorData = (await response.json()) as { error?: string };\n\t\t\t\t\tif (errorData.error) {\n\t\t\t\t\t\terrorMessage = `Proxy error: ${errorData.error}`;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Couldn't parse error response\n\t\t\t\t}\n\t\t\t\tthrow new Error(errorMessage);\n\t\t\t}\n\n\t\t\treader = response.body!.getReader();\n\t\t\tconst decoder = new TextDecoder();\n\t\t\tlet buffer = \"\";\n\n\t\t\twhile (true) {\n\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\tif (done) break;\n\n\t\t\t\tif (options.signal?.aborted) {\n\t\t\t\t\tthrow new Error(\"Request aborted by user\");\n\t\t\t\t}\n\n\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\tbuffer = lines.pop() || \"\";\n","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/earendil-works/pi/blob/4af9d21d3b4d664e4a29fcabfec85171077248e3/packages/agent/src/proxy.ts#L160-L196","documentation":"streamProxy POSTs the model/context/options to {proxyUrl}/api/stream with a Bearer authToken; any non-2xx response becomes 'Proxy error: <status> <statusText>', replaced by the server's JSON {error} field when the body parses. The throw happens inside the stream's async body, so per the StreamFn contract it reaches the caller as an error event / final AssistantMessage with stopReason 'error', not as an exception thrown from streamProxy() itself.","triggerScenarios":"Expired or invalid authToken producing 401/403; wrong or truncated proxyUrl hitting 404/502 from the wrong host; the proxy server failing to reach the upstream LLM provider and returning 500 with its own error message; a gateway in front returning an HTML error page so the JSON override never applies.","commonSituations":"Long-running apps whose token expires mid-session because refresh was never wired; proxyUrl pointing at a different deployment or missing the /api/stream route; nginx/Cloudflare stripping the Authorization header or returning non-JSON error bodies.","solutions":["On 401/403, refresh authToken and retry the stream once","Verify proxyUrl (scheme, host, /api/stream path) with a direct curl POST","Check the proxy server's logs for the failing request to see the upstream cause","If behind a gateway, confirm it forwards the Authorization header and returns JSON errors"],"exampleFix":"// before\nconst stream = streamProxy(model, context, { ...options, authToken, proxyUrl });\n\n// after - consume the final message, refresh token once on 401\nlet token = await getToken();\nfor (let attempt = 0; attempt < 2; attempt++) {\n  const stream = streamProxy(model, context, { ...options, authToken: token, proxyUrl });\n  const msg = await stream.result;\n  if (msg.stopReason !== \"error\" || !/^Proxy error: 40[13]/.test(msg.errorMessage ?? \"\")) break;\n  token = await refreshToken();\n}","handlingStrategy":"try-catch","validationCode":"if (!authToken || !proxyUrl) {\n  throw new Error(\"streamProxy requires authToken and proxyUrl\");\n}\nconst url = new URL(proxyUrl); // throws early on malformed proxyUrl\nawait fetch(url.origin); // optional liveness probe before the real stream","typeGuard":"const isProxyError = (m: { stopReason?: string; errorMessage?: string }): boolean =>\n  m.stopReason === \"error\" && (m.errorMessage ?? \"\").startsWith(\"Proxy error:\");","tryCatchPattern":"// errors arrive on the event stream, not as exceptions\nconst stream = streamProxy(model, context, opts);\nconst msg = await stream.result;\nif (isProxyError(msg)) {\n  const status = /Proxy error: (\\d{3})/.exec(msg.errorMessage ?? \"\")?.[1];\n  if (status === \"401\" || status === \"403\") {\n    authToken = await refreshToken();\n    return streamProxy(model, context, { ...opts, authToken });\n  }\n  throw new Error(msg.errorMessage);\n}","preventionTips":["Refresh auth tokens on a schedule before they expire, not only on failure","Validate proxyUrl configuration at startup","Ensure gateways forward the Authorization header and emit JSON error bodies","Log proxy status codes with request ids to correlate with server logs"],"tags":["proxy","http-status","authentication","streaming","network"],"backgroundTag":"http-error-response","analyzedSha":"4af9d21d3b4d664e4a29fcabfec85171077248e3","analyzedAt":"2026-08-24T13:07:14.692Z","schemaVersion":2},"datasetVersion":"2026-08-24T17:17:21.512Z"}