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

  1. Log response.status plus the raw body text to see exactly what OpenRouter returned
  2. If Cloudflare blocks appear, route requests from an allowed IP or configure proper User-Agent headers
  3. Check OpenRouter's status page and retry transient 5xx with exponential backoff
  4. 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

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.

Related errors


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)