{"record":{"id":"f16ec1538491d9c5","repo":"jackwener/OpenCLI","slug":"twitter-usermedia-pagination-returned-the-same-cur","errorCode":null,"errorMessage":"Twitter UserMedia pagination returned the same cursor twice","messagePattern":"Twitter UserMedia pagination returned the same cursor twice","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/download.js","lineNumber":403,"sourceCode":"    for (let i = 0; i < MAX_PAGINATION_PAGES && all.length < limit; i++) {\n        const fetchCount = nextUserMediaFetchCount(limit, all.length);\n        if (fetchCount === 0) break;\n        const url = buildUserMediaUrl(userMediaOperation, userId, fetchCount, cursor);\n        const data = normalizeTwitterGraphqlPayload(requireFetchPayload(await page.evaluate(`async () => {\n        try {\n          const r = await fetch(\"${url}\", { headers: ${headers}, credentials: 'include' });\n          if (!r.ok) return { ok: false, status: r.status };\n          return { ok: true, payload: await r.json() };\n        } catch (err) {\n          return { ok: false, error: err?.message ?? String(err) };\n        }\n        }`)));\n        const { items, nextCursor } = parseUserMedia(data, seen);\n        all.push(...items);\n        hasMorePages = Boolean(nextCursor);\n        if (!nextCursor) break;\n        if (nextCursor === cursor) {\n            throw new CommandExecutionError('Twitter UserMedia pagination returned the same cursor twice');\n        }\n        cursor = nextCursor;\n    }\n\n    if (all.length === 0) throw new EmptyResultError(`@${username} has no media`, 'Account may be private, suspended, or have no media posts');\n    if (all.length < limit && hasMorePages) {\n        throw new CommandExecutionError(`Twitter UserMedia pagination reached the ${MAX_PAGINATION_PAGES}-page safety cap before collecting ${limit} media items`);\n    }\n\n    const trimmed = all.slice(0, limit);\n    return downloadTwitterMedia(trimmed, {\n        output,\n        subdir: username,\n        cookies: formatCookieHeader(cookies),\n        browserCookies: cookies,\n        filenamePrefix: username,\n        ytdlpExtraArgs: ['--merge-output-format', 'mp4'],\n    });","sourceCodeStart":385,"sourceCodeEnd":421,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/download.js#L385-L421","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"const cursorStalled = (next, prev) => Boolean(next) && next === prev;","tryCatchPattern":"try {\n  await cmd();\n} catch (err) {\n  if (err.message.includes('same cursor twice')) {\n    await sleep(5000);\n    return cmd(); // one retry; the stall is often transient\n  }\n}","preventionTips":["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"],"tags":["pagination","graphql","twitter"],"backgroundTag":"pagination-stuck-cursor","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}