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, tvmazeFetch parses the body with resp.json(). If the body is not valid JSON (HTML error page, empty body, truncated response), it throws a CommandExecutionError including the underlying parse error message. This indicates the endpoint responded but not with the JSON the adapter expects.
Source
Thrown at clis/tvmaze/utils.js:68
}
if (resp.status === 404) {
throw new EmptyResultError(label, `TVmaze returned 404 for ${url}.`);
}
if (resp.status === 429) {
throw new CommandExecutionError(
`${label} returned HTTP 429 (rate limited)`,
'TVmaze caps unauthenticated traffic at ~20 req/10s; 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;
}
const HTML_ENTITY_MAP = {
amp: '&',
lt: '<',
gt: '>',
quot: '"',
apos: "'",
nbsp: ' ',
rsquo: '’',
lsquo: '‘',
rdquo: '”',
ldquo: '“',
hellip: '…',
ndash: '–',
mdash: '—',View on GitHub (pinned to 49907e53dc)
Solutions
- Inspect the underlying parse message — an 'Unexpected token <' usually means HTML was returned instead of JSON
- Check for proxy/VPN/DNS interception of api.tvmaze.com (curl -s https://api.tvmaze.com/shows/1 to see raw output)
- Retry the command; transient truncation may resolve
- Disable TLS-intercepting middleboxes for api.tvmaze.com
Example fix
// before
try { body = await resp.json(); } catch (err) { throw ... }
// after (caller-side pre-check)
const text = await resp.text();
if (!text.trim().startsWith('{') && !text.trim().startsWith('[')) {
throw new CommandExecutionError(`Unexpected non-JSON body: ${text.slice(0, 80)}`);
}
body = JSON.parse(text); Defensive patterns
Strategy: validation
Validate before calling
const text = await resp.text();
if (!text.trim()) throw new Error('empty response body');
JSON.parse(text); // fail fast with your own message before handing to the adapter Type guard
function looksLikeJson(text) { const t = text.trim(); return t.startsWith('{') || t.startsWith('['); } Try / catch
try {
const body = await tvmazeFetch(url, 'search');
} catch (err) {
if (err.message.includes('malformed JSON')) {
console.error('Non-JSON body from api.tvmaze.com — check proxy/DNS interception');
return null;
}
throw err;
} Prevention
- Curl api.tvmaze.com once to confirm raw JSON arrives at your machine
- Exclude the API host from TLS-intercepting proxies/VPNs
- Handle empty 200 bodies defensively
- Retry transient truncation errors
When it happens
Trigger: list or show commands receive a response whose body fails JSON.parse — e.g. a proxy returning HTML, an empty 200 response, or a captive-portal interception page.
Common situations: Corporate proxies or VPNs intercepting api.tvmaze.com traffic, DNS hijacking to a landing page, an intermediate caching layer serving stale/truncated content, or TVmaze briefly serving an error page with a 200 status.
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/4990777f0fabb6d4.
Report an issue: GitHub.