{"record":{"id":"0de29a7bdf61354d","repo":"thedotmack/claude-mem","slug":"parse-error","errorCode":"parse_error","errorMessage":"Anthropic returned invalid JSON","messagePattern":"Anthropic returned invalid JSON","errorType":"exception","errorClass":"ServerClassifiedProviderError","httpStatus":null,"severity":"error","filePath":"src/server/generation/providers/ClaudeObservationProvider.ts","lineNumber":98,"sourceCode":"      });\n    }\n\n    if (!response.ok) {\n      const bodyText = await safeReadBody(response);\n      throw classifyClaudeServerError({\n        status: response.status,\n        bodyText,\n        headers: response.headers,\n        cause: new Error(`Anthropic API error: ${response.status} - ${bodyText}`),\n      });\n    }\n\n    let data: AnthropicMessagesResponse;\n    try {\n      data = (await response.json()) as AnthropicMessagesResponse;\n    } catch (parseError) {\n      const err = parseError instanceof Error ? parseError : new Error(String(parseError));\n      throw new ServerClassifiedProviderError('Anthropic returned invalid JSON', {\n        kind: 'parse_error',\n        cause: err,\n      });\n    }\n\n    if (data.error) {\n      throw classifyClaudeServerError({\n        status: response.status,\n        bodyText: `${data.error.type ?? ''} ${data.error.message ?? ''}`,\n        headers: response.headers,\n        cause: new Error(`Anthropic API error: ${data.error.type} - ${data.error.message}`),\n      });\n    }\n\n    const blocks = Array.isArray(data.content) ? data.content : [];\n    const rawText = blocks\n      .filter(block => block?.type === 'text' && typeof block.text === 'string')\n      .map(block => block.text!)","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/server/generation/providers/ClaudeObservationProvider.ts#L80-L116","documentation":"Thrown by ClaudeObservationProvider.generate() when response.json() rejects after a 2xx HTTP response from Anthropic. Classified as parse_error, it indicates the body was not valid JSON — typically an HTML error page, an empty body, or a truncated/intercepted response. The thrown error carries the underlying parse error as its cause.","triggerScenarios":"A corporate proxy or WAF returns an HTML block page instead of JSON; the upstream returns 200 with an empty or partial body; a gateway truncates the stream; a rate-limit intermediary returns a non-JSON challenge; the endpoint URL points at the wrong host.","commonSituations":"Behind a corporate proxy that rewrites responses. Anthropic serves a temporary HTML status page during an incident. A misconfigured base URL hits a CDN edge returning HTML. Network instability truncates the body mid-stream.","solutions":["Inspect the caught error's cause and the prior status/bodyText logged just before the parse — non-2xx is handled separately, so a 2xx with bad JSON points at interception.","Re-run; transient HTML interception (WAF/proxy) often clears on retry.","If behind a proxy, bypass it for api.anthropic.com or add it to the allowlist.","Confirm the configured base URL resolves to the real Anthropic API host.","If persistent, capture the raw body to confirm whether it is HTML/empty, then report upstream."],"exampleFix":"// before: only status checked, JSON parse throws unexpectedly\n// after: capture bodyText for diagnostics before parsing\nconst bodyText = await response.text();\nif (!bodyText.trim().startsWith('{')) {\n  throw new Error('Anthropic returned non-JSON body');\n}\nconst data = JSON.parse(bodyText) as AnthropicMessagesResponse;","handlingStrategy":"retry","validationCode":"async function fetchJsonOrThrow(response: Response): Promise<unknown> {\n  const bodyText = await response.text();\n  if (!response.ok) {\n    throw new Error(`Anthropic API error: ${response.status} - ${bodyText}`);\n  }\n  if (!bodyText || !bodyText.trim().startsWith('{')) {\n    throw new Error('Anthropic returned a non-JSON body (possible proxy interception)');\n  }\n  return JSON.parse(bodyText);\n}","typeGuard":"function isAnthropicMessagesResponse(v: unknown): v is AnthropicMessagesResponse {\n  return typeof v === 'object' && v !== null\n    && Array.isArray((v as { content?: unknown }).content);\n}","tryCatchPattern":"for (let attempt = 0; attempt < 3; attempt++) {\n  try {\n    return await provider.generate(context);\n  } catch (error) {\n    const isParse = error instanceof ServerClassifiedProviderError && error.kind === 'parse_error';\n    if (!isParse || attempt === 2) throw error;\n    await new Promise(r => setTimeout(r, 2 ** attempt * 500)); // backoff\n  }\n}","preventionTips":["Read the body as text first and inspect its prefix before JSON.parse to distinguish interception from a real parse bug.","Bypass corporate proxies for the provider host or add it to an allowlist.","Verify the configured base URL resolves to the real Anthropic API.","Retry parse errors with exponential backoff since interception is often transient."],"tags":["parse","claude","provider","network","proxy"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}