{"record":{"id":"f3cec2ece95bcf7e","repo":"jackwener/OpenCLI","slug":"xiaoyuzhou-playback-progress-returned-an-unexpecte","errorCode":null,"errorMessage":"Xiaoyuzhou playback progress returned an unexpected response shape","messagePattern":"Xiaoyuzhou playback progress returned an unexpected response shape","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/xiaoyuzhou/history.js","lineNumber":108,"sourceCode":"    }\n    const episode = entry.episode;\n    if (!isRecord(episode.podcast) || typeof episode.isFinished !== 'boolean') {\n        throw new CommandExecutionError(`Xiaoyuzhou history row ${rowNumber} is missing podcast or finished state`);\n    }\n    return {\n        eid: requiredId(episode.eid, `eid in row ${rowNumber}`),\n        pid: requiredId(episode.pid, `pid in row ${rowNumber}`),\n        title: requiredString(episode.title, `title in row ${rowNumber}`),\n        podcast: requiredString(episode.podcast.title, `podcast title in row ${rowNumber}`),\n        durationSec: optionalSeconds(episode.duration, `duration in row ${rowNumber}`, { positive: true }),\n        pubDate: optionalIsoTime(episode.pubDate, `pubDate in row ${rowNumber}`, { required: true }),\n        finished: episode.isFinished,\n    };\n}\n\nfunction parseProgressRows(response, episodes) {\n    if (!Array.isArray(response?.data)) {\n        throw new CommandExecutionError('Xiaoyuzhou playback progress returned an unexpected response shape');\n    }\n    const requested = new Map(episodes.map((episode) => [episode.eid, episode]));\n    const progressById = new Map();\n    for (const [index, row] of response.data.entries()) {\n        if (!isRecord(row)) {\n            throw new CommandExecutionError(`Xiaoyuzhou playback progress row ${index + 1} is malformed`);\n        }\n        const eid = requiredId(row.eid, `progress eid in row ${index + 1}`);\n        const episode = requested.get(eid);\n        if (!episode) {\n            throw new CommandExecutionError(`Xiaoyuzhou playback progress returned unrequested eid ${eid}`);\n        }\n        if (progressById.has(eid)) {\n            throw new CommandExecutionError(`Xiaoyuzhou playback progress returned duplicate eid ${eid}`);\n        }\n        const pid = requiredId(row.pid, `progress pid in row ${index + 1}`);\n        if (pid !== episode.pid) {\n            throw new CommandExecutionError(`Xiaoyuzhou playback progress pid did not match history eid ${eid}`);","sourceCodeStart":90,"sourceCodeEnd":126,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/xiaoyuzhou/history.js#L90-L126","documentation":"parseProgressRows expects the playback-progress endpoint response to have `data` as an array of per-episode progress rows. If response.data is missing or not an array, the progress join cannot proceed, so CommandExecutionError is thrown for the /v1/playback-progress/list call made during `xiaoyuzhou history`.","triggerScenarios":"The progress endpoint returns an error object, null data, or a wrapped object (e.g. { data: { list: [...] } }) instead of a top-level array, right after the history rows for a page were fetched.","commonSituations":"Auth expiry between the two calls returns an error body; API version change wraps the progress list differently; rate limiting returns a JSON error; proxy interference.","solutions":["Re-authenticate and retry (auth expiry between the history and progress calls is the most common cause).","Log the raw progress response body to see the actual shape returned.","Update the CLI/library in case the progress endpoint changed its response wrapper.","Retry after a pause — rate limiting can cause non-list error bodies.","Fix any transport/middleware that might rewrite the response before parsing."],"exampleFix":"// before\n{ \"code\": 401, \"message\": \"token expired\" }\n// after\n{ \"data\": [ { \"eid\": \"...\", \"pid\": \"...\", \"progress\": 300, \"playedAt\": \"2026-08-29T01:00:00Z\" } ] }","handlingStrategy":"try-catch","validationCode":"function hasProgressList(res) { return res !== null && typeof res === 'object' && Array.isArray(res.data); }","typeGuard":"function isProgressResponse(v) { return typeof v === 'object' && v !== null && Array.isArray(v.data); }","tryCatchPattern":"try {\n  const rows = await runHistory();\n} catch (e) {\n  if (e.message === 'Xiaoyuzhou playback progress returned an unexpected response shape') {\n    // refresh credentials and retry once; likely auth expiry or an error body\n  } else throw e;\n}","preventionTips":["Refresh credentials before long history runs so auth survives both API calls","Watch for rate limits; space out large --all fetches","Keep the CLI updated for progress-endpoint wrapper changes"],"tags":["api-response","network","auth","xiaoyuzhou"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}