jackwener/OpenCLI · error · CommandExecutionError

Xiaoyuzhou playback progress returned an unexpected response

Error message

Xiaoyuzhou playback progress returned an unexpected response shape

What it means

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`.

Source

Thrown at clis/xiaoyuzhou/history.js:108

    }
    const episode = entry.episode;
    if (!isRecord(episode.podcast) || typeof episode.isFinished !== 'boolean') {
        throw new CommandExecutionError(`Xiaoyuzhou history row ${rowNumber} is missing podcast or finished state`);
    }
    return {
        eid: requiredId(episode.eid, `eid in row ${rowNumber}`),
        pid: requiredId(episode.pid, `pid in row ${rowNumber}`),
        title: requiredString(episode.title, `title in row ${rowNumber}`),
        podcast: requiredString(episode.podcast.title, `podcast title in row ${rowNumber}`),
        durationSec: optionalSeconds(episode.duration, `duration in row ${rowNumber}`, { positive: true }),
        pubDate: optionalIsoTime(episode.pubDate, `pubDate in row ${rowNumber}`, { required: true }),
        finished: episode.isFinished,
    };
}

function parseProgressRows(response, episodes) {
    if (!Array.isArray(response?.data)) {
        throw new CommandExecutionError('Xiaoyuzhou playback progress returned an unexpected response shape');
    }
    const requested = new Map(episodes.map((episode) => [episode.eid, episode]));
    const progressById = new Map();
    for (const [index, row] of response.data.entries()) {
        if (!isRecord(row)) {
            throw new CommandExecutionError(`Xiaoyuzhou playback progress row ${index + 1} is malformed`);
        }
        const eid = requiredId(row.eid, `progress eid in row ${index + 1}`);
        const episode = requested.get(eid);
        if (!episode) {
            throw new CommandExecutionError(`Xiaoyuzhou playback progress returned unrequested eid ${eid}`);
        }
        if (progressById.has(eid)) {
            throw new CommandExecutionError(`Xiaoyuzhou playback progress returned duplicate eid ${eid}`);
        }
        const pid = requiredId(row.pid, `progress pid in row ${index + 1}`);
        if (pid !== episode.pid) {
            throw new CommandExecutionError(`Xiaoyuzhou playback progress pid did not match history eid ${eid}`);

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Re-authenticate and retry (auth expiry between the history and progress calls is the most common cause).
  2. Log the raw progress response body to see the actual shape returned.
  3. Update the CLI/library in case the progress endpoint changed its response wrapper.
  4. Retry after a pause — rate limiting can cause non-list error bodies.
  5. Fix any transport/middleware that might rewrite the response before parsing.

Example fix

// before
{ "code": 401, "message": "token expired" }
// after
{ "data": [ { "eid": "...", "pid": "...", "progress": 300, "playedAt": "2026-08-29T01:00:00Z" } ] }
Defensive patterns

Strategy: try-catch

Validate before calling

function hasProgressList(res) { return res !== null && typeof res === 'object' && Array.isArray(res.data); }

Type guard

function isProgressResponse(v) { return typeof v === 'object' && v !== null && Array.isArray(v.data); }

Try / catch

try {
  const rows = await runHistory();
} catch (e) {
  if (e.message === 'Xiaoyuzhou playback progress returned an unexpected response shape') {
    // refresh credentials and retry once; likely auth expiry or an error body
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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