jackwener/OpenCLI · error · CommandExecutionError

Xiaoyuzhou history archive did not reach the end of paginati

Error message

Xiaoyuzhou history archive did not reach the end of pagination

What it means

With --all set, fetchHistory promises the complete archive. If pagination ended without observing page.next === null — i.e. exhaustion was never confirmed — it throws rather than returning data that may be silently incomplete.

Source

Thrown at clis/xiaoyuzhou/history.js:230

            break;
        }
        if (page.next === loadMoreKey || seenCursors.has(page.next)) {
            throw new CommandExecutionError('Xiaoyuzhou history pagination repeated the same cursor');
        }
        if (pageNumber === maxPages) {
            throw new CommandExecutionError(
                `Xiaoyuzhou history stopped at the --max-pages safety limit (${maxPages}) before reaching the end`,
            );
        }
        seenCursors.add(page.next);
        loadMoreKey = page.next;
    }

    if (rows.length === 0) {
        throw new EmptyResultError('xiaoyuzhou history', 'The logged-in account has no playback history');
    }
    if (fetchAll && !exhausted) {
        throw new CommandExecutionError('Xiaoyuzhou history archive did not reach the end of pagination');
    }
    return rows;
}

cli({
    site: 'xiaoyuzhou',
    name: 'history',
    access: 'read',
    description: 'List playback history for the logged-in Xiaoyuzhou account',
    domain: 'api.xiaoyuzhoufm.com',
    strategy: Strategy.LOCAL,
    browser: false,
    args: [
        { name: 'limit', type: 'int', default: DEFAULT_LIMIT, help: `Maximum rows to return (default ${DEFAULT_LIMIT}, max ${MAX_LIMIT}). Ignored with --all.` },
        { name: 'all', type: 'bool', default: false, help: 'Fetch every history page until the API cursor is exhausted.' },
        { name: 'max-pages', type: 'int', default: DEFAULT_MAX_PAGES, help: `Pagination safety limit (default ${DEFAULT_MAX_PAGES}, max ${HARD_MAX_PAGES}).` },
    ],
    columns: ['rank', 'eid', 'pid', 'title', 'podcast', 'durationSec', 'progressSec', 'progressPct', 'playedAt', 'pubDate', 'finished', 'url'],

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Inspect the final page's raw response to see how end-of-pagination is signaled and update the exhaustion check (e.g. treat '' or a sentinel as null).
  2. Retry the full --all fetch; transient issues may resolve.
  3. Fall back to --limit/--max-pages with incremental resumption if true exhaustion cannot be detected with the current API.
  4. Verify no middleware/proxy is truncating the final response that carries the null next cursor.

Example fix

// before
if (page.next === null) { exhausted = true; break; }
// after (API signals end with empty string)
if (page.next === null || page.next === '' || page.next === undefined) { exhausted = true; break; }
Defensive patterns

Strategy: try-catch

Try / catch

try {
  return await fetchHistory({ all: true });
} catch (e) {
  if (e instanceof CommandExecutionError && /did not reach the end of pagination/.test(e.message)) {
    console.error('Exhaustion signal unrecognized; check API end-of-pages contract');
  } else throw e;
}

Prevention

When it happens

Trigger: A --all run stops while a next cursor still exists — e.g. the loop ends early after a swallowed transient error, an API returns an unexpected cursor shape so exhaustion is never detected, or cursor handling bugs make page.next never null.

Common situations: API changes the end-of-pagination signal (next becomes '' or a sentinel instead of null); --all runs interrupted by transient errors that a wrapper turns into a normal return; middleware truncating the final empty-next response.

Related errors


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