jackwener/OpenCLI · error · CommandExecutionError
Twitter UserMedia pagination returned the same cursor twice
Error message
Twitter UserMedia pagination returned the same cursor twice
What it means
During UserMedia cursor pagination, if the endpoint returns a nextCursor identical to the cursor just used, the loop would fetch the same page forever. The code detects this and throws CommandExecutionError 'Twitter UserMedia pagination returned the same cursor twice' as an infinite-loop safety guard.
Source
Thrown at clis/twitter/download.js:403
for (let i = 0; i < MAX_PAGINATION_PAGES && all.length < limit; i++) {
const fetchCount = nextUserMediaFetchCount(limit, all.length);
if (fetchCount === 0) break;
const url = buildUserMediaUrl(userMediaOperation, userId, fetchCount, cursor);
const data = normalizeTwitterGraphqlPayload(requireFetchPayload(await page.evaluate(`async () => {
try {
const r = await fetch("${url}", { headers: ${headers}, credentials: 'include' });
if (!r.ok) return { ok: false, status: r.status };
return { ok: true, payload: await r.json() };
} catch (err) {
return { ok: false, error: err?.message ?? String(err) };
}
}`)));
const { items, nextCursor } = parseUserMedia(data, seen);
all.push(...items);
hasMorePages = Boolean(nextCursor);
if (!nextCursor) break;
if (nextCursor === cursor) {
throw new CommandExecutionError('Twitter UserMedia pagination returned the same cursor twice');
}
cursor = nextCursor;
}
if (all.length === 0) throw new EmptyResultError(`@${username} has no media`, 'Account may be private, suspended, or have no media posts');
if (all.length < limit && hasMorePages) {
throw new CommandExecutionError(`Twitter UserMedia pagination reached the ${MAX_PAGINATION_PAGES}-page safety cap before collecting ${limit} media items`);
}
const trimmed = all.slice(0, limit);
return downloadTwitterMedia(trimmed, {
output,
subdir: username,
cookies: formatCookieHeader(cookies),
browserCookies: cookies,
filenamePrefix: username,
ytdlpExtraArgs: ['--merge-output-format', 'mp4'],
});View on GitHub (pinned to 49907e53dc)
Solutions
- Retry the command; this is often transient
- Lower --limit so fewer pages are needed before the loop stops
- Update the CLI if parseUserMedia's cursor extraction no longer matches x.com's response shape
Defensive patterns
Strategy: retry
Type guard
const cursorStalled = (next, prev) => Boolean(next) && next === prev;
Try / catch
try {
await cmd();
} catch (err) {
if (err.message.includes('same cursor twice')) {
await sleep(5000);
return cmd(); // one retry; the stall is often transient
}
} Prevention
- Keep --limit modest to reduce deep pagination
- Space out invocations to avoid rate-limited, malformed responses
- Update parseUserMedia cursor extraction if x.com changes cursor layout
When it happens
Trigger: A UserMedia page response whose bottom cursor entry repeats the incoming cursor: server-side glitch, pinned/unchanged timeline state, or malformed GraphQL response where the cursor extraction picks the wrong entry.
Common situations: x.com GraphQL behaving inconsistently under rate limiting; response shape changes causing the cursor parser to read a static cursor; very old accounts with unusual timeline cursors.
Related errors
- ${probe.detail}
- twitter_bookmarks_repeated_cursor
- twitter_bookmarks_archive_incomplete
- twitter_collection_invalid_timestamp
- twitter_collection_request_error
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/f16ec1538491d9c5.
Report an issue: GitHub.