jackwener/OpenCLI · error · CommandExecutionError

Chess.com callback returned malformed JSON for ${url}: ${err

Error message

Chess.com callback returned malformed JSON for ${url}: ${error?.message || error}

What it means

When resp.ok is true but resp.json() throws, the library rethrows as CommandExecutionError indicating the callback returned malformed JSON. Chess.com's callback endpoint normally returns JSON, but error pages, HTML challenges, or truncated responses will fail to parse.

Source

Thrown at clis/chess/game.js:108

        try {
            resp = await fetch(url, { headers: { 'User-Agent': UA, accept: 'application/json' } });
        } catch (error) {
            throw new CommandExecutionError(`Failed to fetch Chess.com callback ${url}: ${error?.message || error}`);
        }
        if (!resp || typeof resp !== 'object') {
            throw new CommandExecutionError(`Chess.com callback returned an invalid response object for ${url}`);
        }
        if (resp.status === 404) {
            throw new EmptyResultError(`Chess.com has no ${kind} game with id ${id}`);
        }
        if (!resp.ok) {
            throw new CommandExecutionError(`Chess.com callback returned HTTP ${resp.status} for ${url}`);
        }
        let payload;
        try {
            payload = await resp.json();
        } catch (error) {
            throw new CommandExecutionError(`Chess.com callback returned malformed JSON for ${url}: ${error?.message || error}`);
        }
        return [summarizeGame({ kind, id, payload })];
    },
});

export const __test__ = { parseGameUrl, summarizeGame };

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Log or print the raw response text to see what was actually returned (wrap fetch yourself or use curl with the same User-Agent)
  2. Retry after a delay — truncated responses are often transient
  3. Switch to the documented public API (api.chess.com/pub/game/{id}) if the callback endpoint keeps returning HTML
  4. Check for proxy/firewall interference on the host machine

Example fix

// before: silent assumption of JSON
const payload = await resp.json();
// after: capture body for diagnosis
const text = await resp.text();
let payload;
try { payload = JSON.parse(text); }
catch (e) { console.error('Non-JSON body:', text.slice(0, 200)); throw e; }
Defensive patterns

Strategy: try-catch

Try / catch

try {
  const rows = await chessGameCmd(gameUrl);
} catch (e) {
  if (/malformed JSON/.test(e.message)) {
    // retry once after delay; if persistent, capture raw body via curl for diagnosis
    await new Promise(r => setTimeout(r, 3000));
    return await chessGameCmd(gameUrl);
  }
  throw e;
}

Prevention

When it happens

Trigger: The /callback/{kind}/game/{id} endpoint returns 200 with a non-JSON body: an HTML bot-challenge page, an empty body, a truncated response, or a content-type change by Chess.com.

Common situations: Cloudflare/bot interstitial served with status 200; network middleboxes (corporate proxies) rewriting responses; Chess.com changing the callback endpoint format; flaky connections truncating the body.

Understand the failure class

Related errors


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