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, lichessFetch parses the body with resp.json(). If Lichess returns non-JSON content (HTML error page, empty body, CDN block page), JSON.parse fails and this CommandExecutionError is thrown with the underlying parse message. It distinguishes 'server answered but not with JSON' from HTTP-level failures.

Source

Thrown at clis/lichess/utils.js:86

    }
    if (resp.status === 404) {
        throw new EmptyResultError(label, `Lichess returned 404 for ${url}.`);
    }
    if (resp.status === 429) {
        throw new CommandExecutionError(
            `${label} returned HTTP 429 (rate limited)`,
            'Lichess throttles anonymous traffic at ~60 req/min; back off 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;
}

/** Format a lichess unix-ms timestamp as ISO date (YYYY-MM-DD). `null` when missing. */
export function formatTimestamp(ms) {
    if (typeof ms !== 'number' || !Number.isFinite(ms) || ms <= 0) return null;
    const d = new Date(ms);
    if (Number.isNaN(d.getTime())) return null;
    return d.toISOString();
}

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Print/inspect the raw response (curl -v the same URL) to see what non-JSON content is returned.
  2. Check for proxy/VPN/captive-portal interference and bypass it.
  3. Retry the command — often transient.
  4. If persistent, report to Lichess or pin to a different network.

Example fix

// before
const user = await lichessFetch('/api/user/foo');
// after
let user;
try {
  user = await lichessFetch('/api/user/foo');
} catch (e) {
  if (String(e.message).includes('malformed JSON')) {
    console.error('Lichess returned non-JSON (proxy or outage?) — retrying');
    user = await lichessFetch('/api/user/foo');
  } else throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight: confirm the endpoint returns JSON
const res = await fetch('https://lichess.org/api/user/' + encodeURIComponent(user), { headers: { accept: 'application/json' } });
const ct = res.headers.get('content-type') || '';
if (!ct.includes('application/json')) throw new Error('non-JSON response — check proxy/network');

Type guard

function isJsonObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }

Try / catch

try {
  const data = await lichessFetch(path);
} catch (e) {
  if (e.message.includes('malformed JSON')) {
    // transient/proxy issue: inspect raw response and retry once
    return retryWithBackoff(() => lichessFetch(path), 1);
  }
  throw e;
}

Prevention

When it happens

Trigger: resp.json() rejects inside lichessFetch — e.g. an HTML 502 page from a reverse proxy, a Cloudflare challenge page, or an empty response body from a network middlebox, all with a 2xx/ok status.

Common situations: Corporate proxy or VPN intercepting traffic to lichess.org; captive portal Wi-Fi returning HTML; intermittent Lichess CDN issues; antivirus/proxy TLS inspection.

Understand the failure class

Related errors


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