{"record":{"id":"8ad7fd2945c21ef0","repo":"jackwener/OpenCLI","slug":"chess-com-api-returned-an-unexpected-payload-shape","errorCode":null,"errorMessage":"Chess.com API returned an unexpected payload shape for ${url}","messagePattern":"Chess\\.com API returned an unexpected payload shape for (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/chess/utils.js","lineNumber":63,"sourceCode":"    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)) {\n        throw new CommandExecutionError(`Chess.com stats payload for ${kind}.best is not an object`);\n    }\n    if (!isOptionalPlainObject(k.record)) {","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/chess/utils.js#L45-L81","documentation":"chessApi() fetches a Chess.com pub API endpoint and, after confirming the body parses as JSON, verifies the decoded value is a plain (non-null, non-array) object before returning it. This throw means the endpoint responded with valid JSON whose top level is an array, string, number, or null instead of the expected object — i.e. an API contract change or a response from an unexpected origin. The library throws defensively so callers can assume payload.field access is safe.","triggerScenarios":"The URL (path joined to https://api.chess.com/pub/ or an absolute URL passed in) returns JSON that is not an object at the top level, e.g. resp.json() resolves to an array, a bare string/number, or null.","commonSituations":"Chess.com changes an endpoint's response envelope in a new API version; the caller passes a full URL to a non-pub endpoint (proxy, mirror, mock server) that returns a JSON array or scalar; a misconfigured test double returns [] instead of {}; CDN/interstitial returns a JSON string body.","solutions":["Log the URL and JSON.stringify(payload).slice(0,200) to see the actual top-level shape","If the payload is an array, wrap or index it before passing through, or call the correct pub endpoint that returns an object","Update the library/endpoint path if Chess.com changed the pub API response envelope","If using a proxy or mock, fix it to return a JSON object at the top level"],"exampleFix":"// before\nconst list = await chessApi('https://api.chess.com/pub/player/hikori/games/2024/01');\n// after — tolerate an array envelope at the call site\nlet data = await chessApi(url);\nif (Array.isArray(data)) data = { games: data };","handlingStrategy":"type-guard","validationCode":"// Pre-check a fetch/JSON result before trusting chessApi\nfunction assertObjectPayload(json) {\n  if (json === null || typeof json !== 'object' || Array.isArray(json)) {\n    throw new Error(`Unexpected top-level JSON shape: ${Array.isArray(json) ? 'array' : typeof json}`);\n  }\n  return json;\n}","typeGuard":"function isPlainObject(v) {\n  return v !== null && typeof v === 'object' && !Array.isArray(v);\n}\n// usage: if (!isPlainObject(json)) { ...handle... }","tryCatchPattern":"try {\n  const data = await chessApi(path);\n} catch (err) {\n  if (String(err.message).includes('unexpected payload shape')) {\n    console.error(`Contract drift at ${path}: inspect raw body and pin/patch the endpoint`);\n  } else { throw err; }\n}","preventionTips":["Always call documented https://api.chess.com/pub/ endpoints that return a JSON object at top level","Pin/mirror pub API responses in tests to detect contract drift early","Never pass arbitrary absolute URLs to chessApi without checking the response contract","Log the raw body (first ~200 chars) on shape failures for fast diagnosis"],"tags":["api","payload-shape","chess-com","validation"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}