thedotmack/claude-mem · error · ServerClassifiedProviderError
parse_error
parse_error
Error message
OpenRouter returned invalid JSON
What it means
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.
Source
Thrown at src/server/generation/providers/OpenRouterObservationProvider.ts:108
}
if (!response.ok) {
const bodyText = await safeReadBody(response);
throw classifyHttpProviderError({
status: response.status,
bodyText,
headers: response.headers,
cause: new Error(`OpenRouter API error: ${response.status} - ${bodyText}`),
providerLabel: 'OpenRouter',
});
}
let data: OpenRouterResponse;
try {
data = (await response.json()) as OpenRouterResponse;
} catch (parseError) {
const err = parseError instanceof Error ? parseError : new Error(String(parseError));
throw new ServerClassifiedProviderError('OpenRouter returned invalid JSON', {
kind: 'parse_error',
cause: err,
});
}
if (data.error) {
throw classifyHttpProviderError({
status: response.status,
bodyText: `${data.error.code ?? ''} ${data.error.message ?? ''}`,
headers: response.headers,
cause: new Error(`OpenRouter API error: ${data.error.code} - ${data.error.message}`),
providerLabel: 'OpenRouter',
});
}
const rawText = data.choices?.[0]?.message?.content?.trim() ?? '';
if (!rawText) {
logger.warn('SDK', 'OpenRouter returned empty content', {View on GitHub (pinned to d768ba3643)
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.
Example fix
// before: parse without inspecting body shape
// after: guard non-JSON before parsing
const bodyText = await response.text();
if (!bodyText.trim().startsWith('{')) {
throw new Error('OpenRouter returned non-JSON body');
}
const data = JSON.parse(bodyText) as OpenRouterResponse; Defensive patterns
Strategy: retry
Validate before calling
async function fetchOpenRouterJsonOrThrow(response: Response): Promise<unknown> {
const bodyText = await response.text();
if (!response.ok) {
throw new Error(`OpenRouter API error: ${response.status} - ${bodyText}`);
}
if (!bodyText || !bodyText.trim().startsWith('{')) {
throw new Error('OpenRouter returned a non-JSON body (possible proxy interception)');
}
return JSON.parse(bodyText);
} Type guard
function isOpenRouterResponse(v: unknown): v is OpenRouterResponse {
return typeof v === 'object' && v !== null
&& 'choices' in v;
} Try / catch
for (let attempt = 0; attempt < 3; attempt++) {
try {
return await provider.generate(context);
} catch (error) {
const isParse = error instanceof ServerClassifiedProviderError && error.kind === 'parse_error';
if (!isParse || attempt === 2) throw error;
await new Promise(r => setTimeout(r, 2 ** attempt * 500));
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- parse_error
- parse_error
- auth_invalid
- SSE stream returned HTTP ${response.status}
- ${timeoutMessage} (timed out after ${timeoutMs}ms)
AI-assisted analysis of thedotmack/claude-mem@d768ba3643 (2026-08-12).
Data as JSON: /api/errors/7d0bdab3ce1868e2.
Report an issue: GitHub.