{"record":{"id":"610d8c52cb106664","repo":"jackwener/OpenCLI","slug":"xiaoyuzhou-history-row-rownumber-is-malformed","errorCode":null,"errorMessage":"Xiaoyuzhou history row ${rowNumber} is malformed; expected episode metadata","messagePattern":"Xiaoyuzhou history row (.+?) is malformed; expected episode metadata","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/xiaoyuzhou/history.js","lineNumber":89,"sourceCode":"        entries = payload.data;\n        next = payload.loadMoreKey;\n    }\n    else {\n        throw new CommandExecutionError('Xiaoyuzhou history returned an unexpected response shape');\n    }\n    if (next == null || next === '') return { entries, next: null };\n    if (typeof next !== 'string' || !next.trim()) {\n        throw new CommandExecutionError('Xiaoyuzhou history returned an invalid loadMoreKey');\n    }\n    if (entries.length === 0) {\n        throw new CommandExecutionError('Xiaoyuzhou history returned an empty page with a continuation cursor');\n    }\n    return { entries, next: next.trim() };\n}\n\nfunction parseHistoryEpisode(entry, rowNumber) {\n    if (!isRecord(entry) || !isRecord(entry.episode)) {\n        throw new CommandExecutionError(`Xiaoyuzhou history row ${rowNumber} is malformed; expected episode metadata`);\n    }\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)) {","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/xiaoyuzhou/history.js#L71-L107","documentation":"Each history row must be an object containing an `episode` object. parseHistoryEpisode throws this error when an entry is not a record or lacks episode metadata, since every downstream field (eid, pid, title, podcast, duration, pubDate) reads from entry.episode.","triggerScenarios":"During `xiaoyuzhou history`, one of the entries in the page's data array is null, an array, a scalar, or an object without an `episode` key (e.g. the API returned a tombstone/deleted-episode placeholder).","commonSituations":"An episode was deleted from its podcast so the API returns a stub row; API version change nests metadata differently; a proxy strips fields; stale mock fixtures.","solutions":["Retry the command to see if the malformed row is transient.","Update the CLI/library in case the API changed the history row envelope.","Check whether the affected episode was deleted — deleted episodes can yield stub rows; a newer parser version may skip them.","Inspect the raw page payload to confirm which row and shape was returned.","If reproducible, report the row shape upstream so the parser can handle tombstones."],"exampleFix":"// before (deleted-episode tombstone)\n{ \"eid\": \"...\", \"episode\": null }\n// after\n{ \"eid\": \"...\", \"episode\": { \"eid\": \"...\", \"pid\": \"...\", \"title\": \"Ep 1\", \"podcast\": { \"title\": \"P\" }, \"isFinished\": false } }","handlingStrategy":"validation","validationCode":"function hasEpisodeMeta(entry) { return entry !== null && typeof entry === 'object' && !Array.isArray(entry) && entry.episode !== null && typeof entry.episode === 'object'; }","typeGuard":"function isEpisodeEntry(v) { return typeof v === 'object' && v !== null && typeof v.episode === 'object' && v.episode !== null; }","tryCatchPattern":"try {\n  const rows = await runHistory();\n} catch (e) {\n  const m = e.message.match(/row (\\d+) is malformed/);\n  if (m) { /* skip/retry, or log the offending row index */ } else throw e;\n}","preventionTips":["Update the CLI so deleted-episode tombstones are handled by newer parsers","Retry once; partial responses are often transient","Inspect raw page payloads when a specific episode repeatedly fails"],"tags":["api-response","schema-validation","xiaoyuzhou"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}