jackwener/OpenCLI · error · Error

JSON parse failed (status=${response.status}, body[0..50]=${

Error message

JSON parse failed (status=${response.status}, body[0..50]=${JSON.stringify(trimmed.slice(0, 50))}): ${(err instanceof Error ? err.message : String(err))}

What it means

When the response body genuinely is not HTML but JSON.parse still fails, parseJsonOrThrowLoginWall throws a plain Error embedding the HTTP status, the first 50 chars of the body (JSON-stringified), and the underlying parser message. This gives you the actual malformed payload inline so you don't need to reproduce the request to debug it.

Source

Thrown at src/utils.ts:162

    contentType.toLowerCase().includes('text/html')
    || /^<(?:!doctype|html|head|body|title)(?:[\s>/]|$)/i.test(trimmed);

  if (looksLikeHtml) {
    throw new LoginWallError(
      `Server returned HTML instead of JSON (status=${response.status}). `
      + `Likely a login wall, rate limit, or WAF challenge.`,
      response.status,
      opts.url || response.url || '',
      trimmed.slice(0, 100),
    );
  }

  try {
    return JSON.parse(text);
  } catch (err) {
    // Real malformed JSON \u2014 surface body preview alongside the parser message
    // so we don't have to repro to know what came back.
    throw new Error(
      `JSON parse failed (status=${response.status}, body[0..50]=${JSON.stringify(trimmed.slice(0, 50))}): `
      + (err instanceof Error ? err.message : String(err)),
    );
  }
}

/** Browser-side JS source fragment (as a string) that performs a `fetch` and
 * either returns the parsed JSON body or a `LoginWallSignal` sentinel when
 * the response is HTML. Intended to be embedded inside an adapter's
 * `page.evaluate` block.
 *
 * Usage from inside a `page.evaluate` IIFE:
 *
 *     ${BROWSER_JSON_SNIFF_FN}
 *     const res = await fetchJsonOrLoginWall('/some/path.json', { credentials: 'include' });
 *     // res is the parsed JSON object, OR { __loginWall: true, status, url, contentType, bodyPreview }
 *     return res;
 *

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Read the body[0..50] preview in the message to see what actually came back
  2. Retry the request — truncation is often transient (network/proxy)
  3. Log the full response body separately (status + text) when you need the complete payload
  4. If the endpoint consistently returns non-standard content, parse it manually instead of via parseJsonOrThrowLoginWall

Example fix

// before
const data = JSON.parse(text); // opaque SyntaxError
// after
try { const data = JSON.parse(text); }
catch (e) { console.error('status', res.status, 'body:', text.slice(0, 200)); throw e; }
Defensive patterns

Strategy: retry

Validate before calling

const text = await res.text();
if (!text.trim()) throw new Error(`empty body (status ${res.status}) — retry`);

Try / catch

try {
  const data = await parseJsonOrThrowLoginWall(res, { url });
} catch (e) {
  const m = /JSON parse failed \(status=(\d+)/.exec(e.message);
  if (m) { console.error(`Malformed JSON from ${url} (status ${m[1]}) — inspect body preview in message`); }
  throw e;
}

Prevention

When it happens

Trigger: The endpoint returned a 200 (or non-HTML error) response whose body is truncated, empty, partially streamed, or is JSON-ish text with a syntax error — JSON.parse throws inside the try block of parseJsonOrThrowLoginWall.

Common situations: Server-side errors that emit empty bodies with 200; proxies/CDNs truncating large responses; endpoints that changed their response shape (JSONP, NDJSON, or concatenated JSON); timeouts mid-body on flaky networks.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/24df3dbc9120354d. Report an issue: GitHub.