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

  1. Inspect the underlying parse message — an 'Unexpected token <' usually means HTML was returned instead of JSON
  2. Check for proxy/VPN/DNS interception of api.tvmaze.com (curl -s https://api.tvmaze.com/shows/1 to see raw output)
  3. Retry the command; transient truncation may resolve
  4. 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

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

Related errors


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