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

  1. Retry the command; this is often transient
  2. Lower --limit so fewer pages are needed before the loop stops
  3. 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

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


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