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, steamFetch parses the body as JSON. If resp.json() throws (non-JSON body such as HTML, empty body, or truncation), it throws a CommandExecutionError noting the response was malformed JSON.
Source
Thrown at clis/steam/utils.js:82
}
if (resp.status === 429) {
throw new CommandExecutionError(
`${label} returned HTTP 429 (rate limited)`,
'Steam throttles bursty traffic; wait a few seconds and retry.',
);
}
if (resp.status === 404) {
throw new EmptyResultError(label, 'Steam returned 404 — the resource does not exist.');
}
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;
}
export function asString(value) {
return value == null ? '' : String(value);
}
const HTML_ENTITIES = {
'&': '&', '<': '<', '>': '>', '"': '"', ''': "'", ''': "'", ' ': ' ',
};
export function decodeHtmlEntities(value) {
return String(value ?? '')
.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)))
.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(parseInt(d, 10)))
.replace(/&(amp|lt|gt|quot|apos|#39|nbsp);/g, (m) => HTML_ENTITIES[m] || m);
}View on GitHub (pinned to 49907e53dc)
Solutions
- Retry the request; truncation is often transient
- Check the raw response with curl to see what Steam actually returned
- Bypass rewriting proxies/VPN clients that may modify the body
- Look for a bot-challenge page and switch to browser-session mode if available
Example fix
// before
const data = await steamFetch(url, 'app details'); // HTML body -> malformed JSON
// after
let data;
try { data = await steamFetch(url, 'app details'); }
catch (e) { console.error(e.message); data = null; /* fall back to cached value */ } Defensive patterns
Strategy: try-catch
Validate before calling
const pre = await fetch(url, { headers: { accept: 'application/json' } });
const ct = pre.headers.get('content-type') || '';
if (!ct.includes('json')) throw new Error('non-JSON response: ' + ct); Type guard
function isJsonObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); } Try / catch
let data;
try { data = await steamFetch(url, label); }
catch (e) { if (/malformed JSON/.test(e.message)) { data = null; /* fall back to cache */ } else throw e; } Prevention
- Check content-type before parsing when doing raw fetches
- Beware proxies that rewrite responses to HTML challenge pages
- Retry once on parse failure; truncation is often transient
- Validate parsed shape before use
When it happens
Trigger: Steam returning an HTML error/challenge page with a 200 status, empty response bodies, interrupted/truncated responses, or an endpoint that returns text instead of JSON.
Common situations: Bot-protection interstitials served with 200, proxy software rewriting responses, network drops mid-body, calling an endpoint that returns CSV/HTML rather than JSON.
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
- ${label} returned a non-JSON response
- archive snapshots returned malformed JSON: ${error?.message
- COMMAND_EXEC
- `${label} returned malformed JSON: ${err?.message ?? err}`
- ${label} returned malformed JSON: ${err?.message ?? err}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/f6a61e574cf1d6a2.
Report an issue: GitHub.