thedotmack/claude-mem · error · ServerClassifiedProviderError

parse_error

parse_error

Error message

Gemini returned invalid JSON

What it means

The Gemini provider could not parse the HTTP response body from the Google Generative Language API as JSON while running `generate`. The underlying SyntaxError from `response.json()` is wrapped as a ServerClassifiedProviderError with kind 'parse_error'. GeminiResponses that fail include HTML error pages, empty bodies, and malformed payloads.

Solutions

  1. Capture response.status and the first bytes of the raw body to identify what was returned instead of JSON
  2. Confirm the request reaches generativelanguage.googleapis.com without proxy interference (bypass proxy for this host if needed)
  3. Check the model name and API version in the request path — some invalid paths return HTML/empty bodies
  4. Catch the classified parse_error and implement retry-with-backoff for transient gateway failures

Example fix

// before
data = (await response.json()) as GeminiResponse;

// after: surface the real payload on parse failure
const raw = await response.text();
try {
  data = JSON.parse(raw) as GeminiResponse;
} catch (e) {
  throw new ServerClassifiedProviderError(`Gemini 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

const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
new URL(url); // throws early if the composed URL is malformed

Type guard

function isGeminiResponse(v: unknown): v is GeminiResponse {
  return typeof v === 'object' && v !== null &&
    ((v as any).error !== undefined || (v as any).candidates !== undefined || (v as any).promptFeedback !== undefined);
}

Try / catch

try {
  const result = await provider.generate(input);
} catch (e) {
  if (e instanceof ServerClassifiedProviderError && e.kind === 'parse_error') {
    logger.warn('Gemini returned non-JSON body', { cause: e.cause });
    await sleep(backoff());
    return retryWithBackoff(() => provider.generate(input), 3);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling GeminiObservationProvider.generate() when the Gemini endpoint returns a non-JSON body: HTML from proxies or a captive portal, empty 5xx bodies, gzip/streaming truncation, or the URL resolving to something other than the API (bad base URL, blocked region interstitial).

Common situations: VPN/corporate network injecting HTML blocks; Google outage or quota page returned as HTML; misconfigured proxy stripping response bodies; wrong model path returning a non-JSON error page; intermittent truncation on large responses.

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/2f4be348693a3239. Report an issue: GitHub.

Appendix: source

Thrown at src/server/generation/providers/GeminiObservationProvider.ts:188

      });
    }

    if (!response.ok) {
      const bodyText = await safeReadBody(response);
      throw classifyGeminiServerError({
        status: response.status,
        bodyText,
        headers: response.headers,
        cause: new Error(`Gemini HTTP error (status ${response.status})`),
      });
    }

    let data: GeminiResponse;
    try {
      data = (await response.json()) as GeminiResponse;
    } catch (parseError) {
      const err = parseError instanceof Error ? parseError : new Error(String(parseError));
      throw new ServerClassifiedProviderError('Gemini returned invalid JSON', {
        kind: 'parse_error',
        cause: err,
      });
    }

    if (data.error) {
      throw classifyGeminiServerError({
        status: response.status,
        bodyText: `${data.error.status ?? ''} ${data.error.message ?? ''}`,
        headers: response.headers,
        cause: new Error(`Gemini HTTP error (status ${response.status})`),
      });
    }

    const rawText = data.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? '';
    if (!rawText) {
      logger.warn('SDK', 'Gemini returned empty content', { provider: 'gemini', model: this.model });
    }

View on GitHub (pinned to d8bc9755e7)