{"record":{"id":"7d0bdab3ce1868e2","repo":"thedotmack/claude-mem","slug":"parse-error-7d0bda","errorCode":"parse_error","errorMessage":"OpenRouter returned invalid JSON","messagePattern":"OpenRouter returned invalid JSON","errorType":"exception","errorClass":"ServerClassifiedProviderError","httpStatus":null,"severity":"error","filePath":"src/server/generation/providers/OpenRouterObservationProvider.ts","lineNumber":108,"sourceCode":"    }\n\n    if (!response.ok) {\n      const bodyText = await safeReadBody(response);\n      throw classifyHttpProviderError({\n        status: response.status,\n        bodyText,\n        headers: response.headers,\n        cause: new Error(`OpenRouter API error: ${response.status} - ${bodyText}`),\n        providerLabel: 'OpenRouter',\n      });\n    }\n\n    let data: OpenRouterResponse;\n    try {\n      data = (await response.json()) as OpenRouterResponse;\n    } catch (parseError) {\n      const err = parseError instanceof Error ? parseError : new Error(String(parseError));\n      throw new ServerClassifiedProviderError('OpenRouter returned invalid JSON', {\n        kind: 'parse_error',\n        cause: err,\n      });\n    }\n\n    if (data.error) {\n      throw classifyHttpProviderError({\n        status: response.status,\n        bodyText: `${data.error.code ?? ''} ${data.error.message ?? ''}`,\n        headers: response.headers,\n        cause: new Error(`OpenRouter API error: ${data.error.code} - ${data.error.message}`),\n        providerLabel: 'OpenRouter',\n      });\n    }\n\n    const rawText = data.choices?.[0]?.message?.content?.trim() ?? '';\n    if (!rawText) {\n      logger.warn('SDK', 'OpenRouter returned empty content', {","sourceCodeStart":90,"sourceCodeEnd":126,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/server/generation/providers/OpenRouterObservationProvider.ts#L90-L126","documentation":"Thrown by OpenRouterObservationProvider.generate() when response.json() rejects after a successful HTTP status. Classified as parse_error with providerLabel 'OpenRouter', it means the OpenRouter endpoint returned a non-JSON body. The underlying parse error is attached as cause.","triggerScenarios":"A proxy/WAF returns HTML instead of JSON; OpenRouter returns 200 with an empty body; a gateway truncates the stream; the configured apiUrl (resolved via resolveOpenRouterChatCompletionsUrl) points at the wrong host; a transient status page is served.","commonSituations":"Corporate proxy interception. OpenRouter serves an interim HTML page during an incident. A custom baseUrl hits a CDN returning HTML. Network instability truncates the response.","solutions":["Inspect the cause and the status/bodyText captured before parsing.","Retry once; transient interception often clears.","Bypass/allowlist the proxy for the OpenRouter host.","Verify the resolved chat-completions URL is correct.","Capture the raw body to confirm HTML/empty, then escalate."],"exampleFix":"// before: parse without inspecting body shape\n// after: guard non-JSON before parsing\nconst bodyText = await response.text();\nif (!bodyText.trim().startsWith('{')) {\n  throw new Error('OpenRouter returned non-JSON body');\n}\nconst data = JSON.parse(bodyText) as OpenRouterResponse;","handlingStrategy":"retry","validationCode":"async function fetchOpenRouterJsonOrThrow(response: Response): Promise<unknown> {\n  const bodyText = await response.text();\n  if (!response.ok) {\n    throw new Error(`OpenRouter API error: ${response.status} - ${bodyText}`);\n  }\n  if (!bodyText || !bodyText.trim().startsWith('{')) {\n    throw new Error('OpenRouter returned a non-JSON body (possible proxy interception)');\n  }\n  return JSON.parse(bodyText);\n}","typeGuard":"function isOpenRouterResponse(v: unknown): v is OpenRouterResponse {\n  return typeof v === 'object' && v !== null\n    && 'choices' in v;\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));\n  }\n}","preventionTips":["Inspect the raw body prefix before parsing to detect non-JSON responses.","Bypass/allowlist proxies for the OpenRouter host; verify resolveOpenRouterChatCompletionsUrl output.","Retry parse errors with backoff since interception is often transient."],"tags":["parse","openrouter","provider","network","proxy"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}