jackwener/OpenCLI · error · CommandExecutionError

${label} returned invalid JSON

Error message

${label} returned invalid JSON

What it means

fetchJson wraps resp.json() in try/catch and throws CommandExecutionError '<label> returned invalid JSON' when the response body cannot be parsed. The server returned 2xx but the body is not valid JSON (HTML error page, empty body, truncated response, or an anti-bot interstitial).

Source

Thrown at clis/weread/book-search.js:94

async function fetchJson(url, label) {
    let resp;
    try {
        resp = await fetch(url.toString(), {
            headers: { 'User-Agent': WEREAD_UA },
        });
    }
    catch (error) {
        throw new CommandExecutionError(`${label} request failed: ${error instanceof Error ? error.message : String(error)}`);
    }
    if (!resp.ok) {
        throw new CommandExecutionError(`${label} request failed: HTTP ${resp.status}`);
    }
    try {
        return await resp.json();
    }
    catch {
        throw new CommandExecutionError(`${label} returned invalid JSON`);
    }
}

async function fetchText(url, label) {
    let resp;
    try {
        resp = await fetch(url.toString(), {
            headers: { 'User-Agent': WEREAD_UA },
        });
    }
    catch (error) {
        throw new CommandExecutionError(`${label} request failed: ${error instanceof Error ? error.message : String(error)}`);
    }
    if (!resp.ok) {
        throw new CommandExecutionError(`${label} request failed: HTTP ${resp.status}`);
    }
    return resp.text();
}

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Inspect the raw response with curl (same URL and User-Agent) to see what the body actually is
  2. Retry — truncated/intermittent responses may succeed on retry
  3. Check for proxy/captive-portal interference and bypass it
  4. If WeRead now requires auth or changed the response format, update the CLI
  5. Log the first bytes of the body before parsing in a patched build to diagnose

Example fix

// before
let data;
try { data = await fetchJson(url, 'WeRead book search'); } catch (e) { throw e; }
// after (fetch raw text to diagnose non-JSON bodies)
const resp = await fetch(url, { headers: { 'User-Agent': WEREAD_UA } });
const text = await resp.text();
if (!text.trim().startsWith('{') && !text.trim().startsWith('[')) {
  throw new Error('Non-JSON body (first 200 chars): ' + text.slice(0, 200));
}
const data = JSON.parse(text);
Defensive patterns

Strategy: fallback

Validate before calling

// Inspect the raw body shape before trusting json parsing
const resp = await fetch(url, { headers: { 'User-Agent': WEREAD_UA } });
const text = await resp.text();
const looksJson = /^[\s]*[[{]/.test(text);
if (!looksJson) throw new Error('Non-JSON response: ' + text.slice(0, 120));

Type guard

const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);

Try / catch

try {
  const data = await fetchJson(url, 'WeRead book search');
} catch (e) {
  if (e instanceof CommandExecutionError && e.message.includes('returned invalid JSON')) {
    // fallback: log body, or fall back to HTML scraping path
    console.error('WeRead returned a non-JSON body (login page/proxy?).');
  } else throw e;
}

Prevention

When it happens

Trigger: WeRead returns an HTML login/captcha page with status 200, an empty body, a proxy injects its own HTML error page, or the response is truncated mid-transfer causing JSON.parse to fail.

Common situations: Captive portals or corporate proxies rewriting 200 responses, session/login requirements added server-side, CDN returning cached HTML for an API path, or intermittent truncation on flaky connections.

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 jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/c4b50014e6e2ad69. Report an issue: GitHub.