jackwener/OpenCLI · error · CommandExecutionError
${label} returned malformed JSON: ${err?.message ?? err}
Error message
${label} returned malformed JSON: ${err?.message ?? err} What it means
After a successful HTTP response, lichessFetch parses the body with resp.json(). If Lichess returns non-JSON content (HTML error page, empty body, CDN block page), JSON.parse fails and this CommandExecutionError is thrown with the underlying parse message. It distinguishes 'server answered but not with JSON' from HTTP-level failures.
Source
Thrown at clis/lichess/utils.js:86
}
if (resp.status === 404) {
throw new EmptyResultError(label, `Lichess returned 404 for ${url}.`);
}
if (resp.status === 429) {
throw new CommandExecutionError(
`${label} returned HTTP 429 (rate limited)`,
'Lichess throttles anonymous traffic at ~60 req/min; back off and retry.',
);
}
if (!resp.ok) {
throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
}
let body;
try {
body = await resp.json();
}
catch (err) {
throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
}
return body;
}
/** Format a lichess unix-ms timestamp as ISO date (YYYY-MM-DD). `null` when missing. */
export function formatTimestamp(ms) {
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms <= 0) return null;
const d = new Date(ms);
if (Number.isNaN(d.getTime())) return null;
return d.toISOString();
}
View on GitHub (pinned to 49907e53dc)
Solutions
- Print/inspect the raw response (curl -v the same URL) to see what non-JSON content is returned.
- Check for proxy/VPN/captive-portal interference and bypass it.
- Retry the command — often transient.
- If persistent, report to Lichess or pin to a different network.
Example fix
// before
const user = await lichessFetch('/api/user/foo');
// after
let user;
try {
user = await lichessFetch('/api/user/foo');
} catch (e) {
if (String(e.message).includes('malformed JSON')) {
console.error('Lichess returned non-JSON (proxy or outage?) — retrying');
user = await lichessFetch('/api/user/foo');
} else throw e;
} Defensive patterns
Strategy: retry
Validate before calling
// Pre-flight: confirm the endpoint returns JSON
const res = await fetch('https://lichess.org/api/user/' + encodeURIComponent(user), { headers: { accept: 'application/json' } });
const ct = res.headers.get('content-type') || '';
if (!ct.includes('application/json')) throw new Error('non-JSON response — check proxy/network'); Type guard
function isJsonObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); } Try / catch
try {
const data = await lichessFetch(path);
} catch (e) {
if (e.message.includes('malformed JSON')) {
// transient/proxy issue: inspect raw response and retry once
return retryWithBackoff(() => lichessFetch(path), 1);
}
throw e;
} Prevention
- Set Accept: application/json on all requests.
- Bypass suspicious proxies/VPNs when calling lichess.org.
- Retry once on parse errors — they are often transient middlebox artifacts.
- Log the raw response body when debugging to see the HTML interstitial.
When it happens
Trigger: resp.json() rejects inside lichessFetch — e.g. an HTML 502 page from a reverse proxy, a Cloudflare challenge page, or an empty response body from a network middlebox, all with a 2xx/ok status.
Common situations: Corporate proxy or VPN intercepting traffic to lichess.org; captive portal Wi-Fi returning HTML; intermittent Lichess CDN issues; antivirus/proxy TLS inspection.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- archive search returned malformed JSON: ${error?.message ||
- archive wayback returned malformed JSON: ${error?.message ||
- ${label} returned malformed JSON: ${err?.message ?? err}
- hf models returned malformed JSON: ${error?.message || error
- ${label} returned malformed JSON: ${err?.message ?? err}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/efda0b705fae88f3.
Report an issue: GitHub.