thedotmack/claude-mem · error · ServerClassifiedProviderError

parse_error

parse_error

Error message

Gemini returned invalid JSON

What it means

Thrown by GeminiObservationProvider.generate() when response.json() rejects after a successful HTTP status. Classified as parse_error, it means the Gemini endpoint returned a non-JSON body (HTML, empty, or truncated). The underlying parse error is attached as cause.

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 d768ba3643)

Solutions

  1. Inspect the cause and the status/bodyText captured just before parsing — a 2xx with non-JSON points at interception.
  2. Retry once; transient interception often clears.
  3. Bypass or allowlist the proxy for the Gemini host.
  4. Verify the base URL resolves to the real Gemini API.
  5. 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('Gemini returned non-JSON body');
}
const data = JSON.parse(bodyText) as GeminiResponse;
Defensive patterns

Strategy: retry

Validate before calling

async function fetchGeminiJsonOrThrow(response: Response): Promise<unknown> {
  const bodyText = await response.text();
  if (!response.ok) {
    throw new Error(`Gemini HTTP error (status ${response.status})`);
  }
  if (!bodyText || !bodyText.trim().startsWith('{')) {
    throw new Error('Gemini returned a non-JSON body (possible proxy interception)');
  }
  return JSON.parse(bodyText);
}

Type guard

function isGeminiResponse(v: unknown): v is GeminiResponse {
  return typeof v === 'object' && v !== null
    && 'candidates' 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

When it happens

Trigger: A proxy/WAF returns HTML in place of the JSON response; the Gemini endpoint returns 200 with an empty body; a gateway truncates the stream; the configured base URL points at the wrong host; a transient Google status page is served.

Common situations: Corporate proxy interception. Google serves an interim HTML page during an outage. A wrong base URL hits a CDN returning HTML. Network instability truncates the response.

Understand the failure class

Related errors


AI-assisted analysis of thedotmack/claude-mem@d768ba3643 (2026-08-12). Data as JSON: /api/errors/2f4be348693a3239. Report an issue: GitHub.