jackwener/OpenCLI · error · CliError

PARSE_ERROR

PARSE_ERROR

Error message

PARSE_ERROR: Invalid JSON response for ${path}

What it means

Thrown by fetchWebApi when the response from a public WeRead web endpoint has HTTP success status but its body cannot be parsed as JSON (resp.json() rejects). The hint notes WeRead may have returned an HTML error page — meaning the request looked successful to HTTP semantics but the payload is not the expected JSON.

Source

Thrown at clis/weread/utils.js:120

 * Used by search and ranking commands (browser: false).
 */
export async function fetchWebApi(path, params) {
    const url = new URL(`${WEB_API}${path}`);
    if (params) {
        for (const [k, v] of Object.entries(params))
            url.searchParams.set(k, v);
    }
    const resp = await fetch(url.toString(), {
        headers: { 'User-Agent': WEREAD_UA },
    });
    if (!resp.ok) {
        throw new CliError('FETCH_ERROR', `HTTP ${resp.status} for ${path}`, 'WeRead API may be temporarily unavailable');
    }
    try {
        return await resp.json();
    }
    catch {
        throw new CliError('PARSE_ERROR', `Invalid JSON response for ${path}`, 'WeRead may have returned an HTML error page');
    }
}
/**
 * Fetch a private WeRead API endpoint with cookies extracted from the browser.
 * The HTTP request itself runs in Node.js to avoid page-context CORS failures.
 *
 * Cookies are collected from both the API subdomain (i.weread.qq.com) and the
 * main domain (weread.qq.com). WeRead may set auth cookies as host-only on
 * weread.qq.com, which won't match i.weread.qq.com in a URL-based lookup.
 */
export async function fetchPrivateApi(page, path, params) {
    const url = new URL(`${API}${path}`);
    if (params) {
        for (const [k, v] of Object.entries(params))
            url.searchParams.set(k, v);
    }
    const urlStr = url.toString();
    // Merge cookies from both domains; API-domain cookies take precedence on name collision

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Capture resp.text() and inspect the first bytes — if it's HTML ("<!DOCTYPE" or a captcha page), the request is being challenged; add cookies or browser-like headers.
  2. Retry the request — a truncated transfer or transient proxy issue often resolves on retry.
  3. Bypass suspect proxies/VPN and test direct connectivity with curl to compare bodies.
  4. If a WAF challenge page is returned, route requests through a real browser session or a headless browser instead of raw fetch.
  5. Catch CliError with code 'PARSE_ERROR' and fall back to an alternative data source (HTML scraping path).

Example fix

// before
try { return await resp.json(); } catch { throw new CliError('PARSE_ERROR', ...); }
// after
const text = await resp.text();
try { return JSON.parse(text); }
catch { throw new CliError('PARSE_ERROR', `Invalid JSON for ${path}; body starts with: ${text.slice(0, 120)}`); }
Defensive patterns

Strategy: retry

Try / catch

try {
  const data = await callWereadPublicApi();
} catch (e) {
  if (e.code === 'PARSE_ERROR') {
    // HTML error/captcha page likely; back off, or reroute through a browser session
    await sleep(5000);
    return retryOnceOrFallbackToScraping();
  }
  throw e;
}

Prevention

When it happens

Trigger: resp.json() throws on the /web/* response body: an HTML captcha/WAF challenge page served with 200, an empty body, truncated/garbled response from a proxy, or Content-Type mismatch where the body is HTML text.

Common situations: Captive portals or corporate MITM proxies injecting HTML; WeRead anti-bot serving an HTML challenge with status 200; a CDN error page returned with 200; network middleware corrupting the response mid-transfer.

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