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
- Inspect the raw response with curl (same URL and User-Agent) to see what the body actually is
- Retry — truncated/intermittent responses may succeed on retry
- Check for proxy/captive-portal interference and bypass it
- If WeRead now requires auth or changed the response format, update the CLI
- 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
- Check for captive portals/proxies that rewrite 200 responses into HTML
- Log or capture the raw body on failure to distinguish captcha pages from truncation
- Retry once on parse failure — truncated bodies are often transient
- Keep the CLI updated for auth/response-format changes on WeRead's side
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.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- 12306 queryByTrainNo returned non-JSON body
- 12306 ${endpoint} returned non-JSON body
- 12306 ${endpoint} returned an unexpected payload shape
- 12306 rejected every known query endpoint name (${QUERY_ENDP
- coingecko derivatives returned malformed JSON: ${err?.messag
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/c4b50014e6e2ad69.
Report an issue: GitHub.