thedotmack/claude-mem · error · ServerClassifiedProviderError
OpenRouter returned invalid JSON
Error message
OpenRouter returned invalid JSON
What it means
The OpenRouter provider could not deserialize the HTTP response body into OpenRouterResponse during `generate`; `response.json()` threw and the provider re-threw it as a ServerClassifiedProviderError with kind 'parse_error'. OpenRouter sometimes returns non-JSON bodies such as Cloudflare HTML blocks or empty gateway-error pages.
Solutions
- Log response.status plus the raw body text to see exactly what OpenRouter returned
- If Cloudflare blocks appear, route requests from an allowed IP or configure proper User-Agent headers
- Check OpenRouter's status page and retry transient 5xx with exponential backoff
- Catch kind 'parse_error' ServerClassifiedProviderError and fall back to another provider or surface a user-readable message
Example fix
// before
data = (await response.json()) as OpenRouterResponse;
// after
const raw = await response.text();
try {
data = JSON.parse(raw) as OpenRouterResponse;
} catch (e) {
throw new ServerClassifiedProviderError(`OpenRouter returned non-JSON (status ${response.status}): ${raw.slice(0, 200)}`, { kind: 'parse_error', cause: e as Error });
} Defensive patterns
Strategy: try-catch
Validate before calling
// preflight: verify reachability before real calls
const res = await fetch('https://openrouter.ai/api/v1/models');
if (!res.ok || !(res.headers.get('content-type') ?? '').includes('application/json')) {
throw new Error('OpenRouter endpoint not returning JSON; check network/proxy');
} Type guard
function isOpenRouterResponse(v: unknown): v is OpenRouterResponse {
return typeof v === 'object' && v !== null &&
((v as any).error !== undefined || (v as any).choices !== undefined);
} Try / catch
try {
const result = await provider.generate(input);
} catch (e) {
if (e instanceof ServerClassifiedProviderError && e.kind === 'parse_error') {
logger.warn('OpenRouter returned non-JSON body (likely Cloudflare/gateway)', { cause: e.cause });
return fallbackProvider.generate(input);
}
throw e;
} Prevention
- Log response.status and content-type before parsing to detect Cloudflare/proxy pages
- Send a descriptive User-Agent and avoid flagged datacenter IPs to reduce Cloudflare blocks
- Implement retry-with-backoff plus a fallback provider for transient gateway failures
- Check the OpenRouter status page when parse errors spike
When it happens
Trigger: Calling OpenRouterObservationProvider.generate() when the openrouter.ai endpoint returns a non-JSON body: Cloudflare challenge/block pages, empty 502/504 bodies, rate-limit interstitials, or truncated streaming responses.
Common situations: Datacenter IP triggering Cloudflare bot protection; OpenRouter incident returning HTML; intermediary proxy stripping or truncating bodies; wrong model id causing an upstream failure surfaced as a non-JSON error page.
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
- 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
- API key prompt cancelled — falling back to Claude provider.
- auth_invalid
- classifyHttpProviderError
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/e38317cbc88e9326.
Report an issue: GitHub.
Appendix: source
Thrown at src/server/generation/providers/OpenRouterObservationProvider.ts:110
}
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 d8bc9755e7)