{"record":{"id":"4d217d390e583fd0","repo":"jackwener/OpenCLI","slug":"chess-com-api-returned-malformed-json-for-url","errorCode":null,"errorMessage":"Chess.com API returned malformed JSON for ${url}: ${error?.message || error}","messagePattern":"Chess\\.com API returned malformed JSON for (.+?): (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/chess/utils.js","lineNumber":60,"sourceCode":"\nexport async function chessApi(path, fetchImpl = fetch) {\n    const url = path.startsWith('http') ? path : `${API_BASE}${path}`;\n    let resp;\n    try {\n        resp = await fetchImpl(url, { headers: { 'User-Agent': UA, accept: 'application/json' } });\n    } catch (error) {\n        throw new CommandExecutionError(`Failed to fetch Chess.com API ${url}: ${error?.message || error}`);\n    }\n    if (!resp || typeof resp !== 'object') {\n        throw new CommandExecutionError(`Chess.com API returned an invalid response object for ${url}`);\n    }\n    if (resp.status === 404) throw new EmptyResultError(`Chess.com returned 404 for ${url}`);\n    if (!resp.ok) throw new CommandExecutionError(`Chess.com API returned HTTP ${resp.status} for ${url}`);\n    let payload;\n    try {\n        payload = await resp.json();\n    } catch (error) {\n        throw new CommandExecutionError(`Chess.com API returned malformed JSON for ${url}: ${error?.message || error}`);\n    }\n    if (!isPlainObject(payload)) {\n        throw new CommandExecutionError(`Chess.com API returned an unexpected payload shape for ${url}`);\n    }\n    return payload;\n}\n\n/** Pull rating + record fields out of a stats sub-object (`chess_rapid` etc). */\nexport function summarizeStats(stats, kind) {\n    const k = stats?.[kind];\n    if (!k) return null;\n    if (!isPlainObject(k)) {\n        throw new CommandExecutionError(`Chess.com stats payload for ${kind} is not an object`);\n    }\n    if (!isOptionalPlainObject(k.last)) {\n        throw new CommandExecutionError(`Chess.com stats payload for ${kind}.last is not an object`);\n    }\n    if (!isOptionalPlainObject(k.best)) {","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/chess/utils.js#L42-L78","documentation":"A CommandExecutionError thrown when resp.json() rejects because the body is not valid JSON. The library wraps the underlying parse error (with its message) alongside the requested URL to pinpoint which endpoint returned the bad body.","triggerScenarios":"The Chess.com endpoint or an intermediary (proxy, captive portal, HTML error page from a load balancer) returns HTML or an empty body with a 200 status; a custom fetchImpl stub returns a non-JSON body string.","commonSituations":"Captive Wi-Fi portals injecting HTML into all responses; corporate proxies replacing error bodies; CDN edge errors behind a 200; test stubs like new Response('ok') without JSON content.","solutions":["Retry the request — transient proxy/CDN glitches often resolve.","curl the exact URL from the error message and inspect whether the body is HTML or empty.","Check for captive-portal/proxy interference (try a different network).","If you inject a fetchImpl in tests, make its Response body valid JSON."],"exampleFix":"// before\nchessApi(path, async () => new Response('<html>blocked</html>', { status: 200 })); // throws here\n// after\nchessApi(path, async () => new Response(JSON.stringify({ chess_rapid: {} }), { status: 200, headers: { 'content-type': 'application/json' } }));","handlingStrategy":"retry","validationCode":"// Check the body is actually JSON before heavy processing:\nconst resp = await fetch(url, { headers: { accept: 'application/json' } });\nconst text = await resp.text();\ntry { JSON.parse(text); } catch { throw new Error(`Non-JSON body from ${url}: ${text.slice(0, 120)}`); }","typeGuard":"function isMalformedJsonError(e) {\n  return e instanceof Error && /malformed JSON/.test(e.message);\n}","tryCatchPattern":"async function fetchJsonWithRetry(fn, { retries = 2 } = {}) {\n  for (let i = 0; ; i++) {\n    try { return await fn(); }\n    catch (e) {\n      if (!isMalformedJsonError(e) || i >= retries) throw e;\n      await new Promise((r) => setTimeout(r, 1000 * (i + 1)));\n    }\n  }\n}","preventionTips":["Set the accept: application/json header (chessApi already does).","Retry once or twice — edge/CDN glitches are usually transient.","Check for captive portals or proxies injecting HTML on flaky networks.","In tests, make mocked Response bodies valid JSON."],"tags":["json","malformed-response","network","chess-com"],"backgroundTag":"invalid-json-response","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}