jackwener/OpenCLI · error · CommandExecutionError

Xiaoyuzhou history returned an invalid loadMoreKey

Error message

Xiaoyuzhou history returned an invalid loadMoreKey

What it means

When a pagination cursor (loadMoreKey) is present, it must be a non-empty string; the library trims it and passes it back as the next request body. A cursor that is a number, object, null-like, or whitespace-only string cannot be safely forwarded, so CommandExecutionError is thrown instead of issuing a malformed continuation request.

Source

Thrown at clis/xiaoyuzhou/history.js:79

    }
    const payload = response?.data;
    let entries;
    let next;
    if (Array.isArray(payload)) {
        entries = payload;
        next = response.raw?.loadMoreKey;
    }
    else if (isRecord(payload) && Array.isArray(payload.data)
        && !Object.prototype.hasOwnProperty.call(response.raw, 'loadMoreKey')) {
        entries = payload.data;
        next = payload.loadMoreKey;
    }
    else {
        throw new CommandExecutionError('Xiaoyuzhou history returned an unexpected response shape');
    }
    if (next == null || next === '') return { entries, next: null };
    if (typeof next !== 'string' || !next.trim()) {
        throw new CommandExecutionError('Xiaoyuzhou history returned an invalid loadMoreKey');
    }
    if (entries.length === 0) {
        throw new CommandExecutionError('Xiaoyuzhou history returned an empty page with a continuation cursor');
    }
    return { entries, next: next.trim() };
}

function parseHistoryEpisode(entry, rowNumber) {
    if (!isRecord(entry) || !isRecord(entry.episode)) {
        throw new CommandExecutionError(`Xiaoyuzhou history row ${rowNumber} is malformed; expected episode metadata`);
    }
    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}`),

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Log the raw loadMoreKey value from the failing page to see its actual type/content.
  2. Retry the command — a transiently truncated response can produce an empty cursor string.
  3. Update the CLI/library if the API switched to a non-string cursor format.
  4. Fetch with a smaller --limit (or without --all) to reduce pages traversed, sidestepping the bad cursor.
  5. Report/patch parseHistoryPage to normalize the new cursor format.

Example fix

// before
{ "loadMoreKey": { "offset": 20 } }
// after (expected string cursor)
{ "loadMoreKey": "eyJvZmZzZXQiOjIwfQ==" }
Defensive patterns

Strategy: validation

Validate before calling

function isValidCursor(k) { return k == null || k === '' || (typeof k === 'string' && k.trim().length > 0); }
// verify before paginating: every page.next satisfies isValidCursor

Type guard

function isNonEmptyString(v) { return typeof v === 'string' && v.trim().length > 0; }

Try / catch

try {
  const rows = await runHistory();
} catch (e) {
  if (e.message === 'Xiaoyuzhou history returned an invalid loadMoreKey') {
    // stop pagination and return rows collected so far
  } else throw e;
}

Prevention

When it happens

Trigger: The history API returns loadMoreKey as a non-string (number, nested object) or an all-whitespace string while more pages remain, during `xiaoyuzhou history` or `--all` pagination.

Common situations: API version change alters cursor encoding (e.g. opaque object cursor instead of string token); a transparent proxy truncates the cursor; a mock fixture uses a placeholder cursor value.

Related errors


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