jackwener/OpenCLI · error · CommandExecutionError
`${label} returned malformed JSON: ${err?.message ?? err}`
Error message
`${label} returned malformed JSON: ${err?.message ?? err}` What it means
eolFetch wraps fetch calls to endoflife.date and expects an HTML/JSON response whose body parses as JSON. When resp.json() throws — meaning the server returned non-JSON (HTML error page, empty body, truncated response) — it rethrows as CommandExecutionError with the label prefixed so you know which endpoint failed. The underlying parse error message is appended after 'returned malformed JSON:'.
Source
Thrown at clis/endoflife/utils.js:70
}
if (resp.status === 404) {
throw new EmptyResultError(label, `endoflife.date returned 404 for ${url}.`);
}
if (resp.status === 429) {
throw new CommandExecutionError(
`${label} returned HTTP 429 (rate limited)`,
'endoflife.date throttles unauthenticated traffic; wait a few seconds 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;
}
// endoflife.date returns scalar fields that are either an ISO date "YYYY-MM-DD",
// a boolean (true = supported / ongoing, false = not LTS), or null. Normalise:
// - boolean true -> the literal string "ongoing"
// - boolean false -> null (matches "no LTS phase" / "not in extended support")
// - date string -> as-is
// - anything else -> null
export function normaliseDateOrFlag(value) {
if (value === true) return 'ongoing';
if (value === false || value == null) return null;
if (typeof value === 'string') {
const s = value.trim();
return s || null;
}
return null;View on GitHub (pinned to 49907e53dc)
Solutions
- Re-run the command — transient truncations or Cloudflare blips often resolve on retry.
- Check status with curl -s https://endoflife.date/api/all | head to see what the server actually returns.
- Check for a corporate proxy/firewall injecting an HTML block page and add an exception for endoflife.date.
- Inspect the CommandExecutionError message: the text after 'returned malformed JSON:' is the underlying fetch JSON parse error (e.g. 'Unexpected token < in JSON').
Example fix
// before
const cycles = await cycles('nodejs');
// after
let cycles;
try {
cycles = await cycles('nodejs');
} catch (err) {
if (err instanceof CommandExecutionError && err.message.includes('returned malformed JSON')) {
cycles = await cycles('nodejs'); // retry once on transient non-JSON response
} else {
throw err;
}
} Defensive patterns
Strategy: try-catch
Validate before calling
const resp = await fetch(url);
const text = await resp.text();
if (!resp.ok || !text.trim().startsWith('{') && !text.trim().startsWith('[')) {
throw new Error(`endoflife.date returned non-JSON (HTTP ${resp.status})`);
} Type guard
function isJsonObject(v) {
return v !== null && typeof v === 'object' && !Array.isArray(v);
} Try / catch
try {
const data = await eolFetch(url, label);
} catch (err) {
if (err instanceof CommandExecutionError && err.message.includes('returned malformed JSON')) {
// fall back to cached data or retry with backoff
} else {
throw err;
}
} Prevention
- Wrap eolFetch calls in a retry with exponential backoff for transient bad responses.
- Check resp.status and content-type before calling resp.json() in custom fetch paths.
- Cache successful endoflife.date responses to survive outages.
- Monitor the raw response body when running behind corporate proxies.
When it happens
Trigger: Calling cycles (or any eolFetch consumer) when endoflife.date responds with non-JSON: an HTML 5xx/Cloudflare error page, a 404 body, a rate-limit page, or an empty/truncated body, so resp.json() throws.
Common situations: endoflife.date outage or Cloudflare challenge page; network middlebox/proxy injecting an HTML block page; hitting the wrong URL after an API path change; intermittent connection drop truncating the response mid-body.
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 snapshots returned malformed JSON: ${error?.message
- ${label} returned malformed JSON: ${err?.message ?? err}
- linux.do request failed: HTTP ${result.status ?? 'unknown'}
- mdn search request failed: ${err?.message ?? err}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/8e24ba6d97f39bc8.
Report an issue: GitHub.