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 = {
    '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&apos;': "'", '&#39;': "'", '&nbsp;': ' ',
};

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

  1. Retry the request; truncation is often transient
  2. Check the raw response with curl to see what Steam actually returned
  3. Bypass rewriting proxies/VPN clients that may modify the body
  4. 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

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.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/f6a61e574cf1d6a2. Report an issue: GitHub.