jackwener/OpenCLI · error · CommandExecutionError

Bookmarks

Error message

Bookmarks

What it means

The Bookmarks GraphQL endpoint returned an HTTP error status. When the very first page fails (zero records collected so far), the CLI throws CommandExecutionError with describeTwitterApiError('Bookmarks', status) instead of silently stopping, because an empty archive from a failed first request is never a valid outcome. The 'Bookmarks' in the message is the endpoint name passed to the formatter, which composes the final description from the returned HTTP status code.

Source

Thrown at clis/twitter/bookmarks.js:250

        }
        let outputCount = useOutputFile ? jsonlState.count : 0;
        let cursor = resumed?.cursor || null;
        let pages = 0;
        let exhausted = false;
        // Runaway guard only; --limit/--all and cursor exhaustion control normal pagination.
        while (pages < maxPages && (fetchAll || allTweets.length < limit)) {
            pages += 1;
            const currentCount = useOutputFile ? outputCount : allTweets.length;
            const remaining = fetchAll ? 100 : (limit - currentCount + 10);
            const fetchCount = Math.min(100, remaining);
            const apiUrl = buildBookmarksUrl(fetchCount, cursor).replace(BOOKMARKS_QUERY_ID, queryId);
            const data = unwrapBrowserResult(await page.evaluate(`async () => {
        const r = await fetch(${JSON.stringify(apiUrl)}, { headers: ${headers}, credentials: 'include' });
        return r.ok ? await r.json() : { error: r.status };
      }`));
            if (data?.error) {
                if ((useOutputFile ? outputCount : allTweets.length) === 0)
                    throw new CommandExecutionError(describeTwitterApiError('Bookmarks', data.error));
                break;
            }
            const hasInstructions = Array.isArray(data?.data?.bookmark_timeline_v2?.timeline?.instructions)
                || Array.isArray(data?.data?.bookmark_timeline?.timeline?.instructions);
            if (!hasInstructions) {
                throw new CommandExecutionError('twitter_bookmarks_protocol_error: missing Bookmarks timeline instructions');
            }
            const { tweets, nextCursor } = parseBookmarks(data, seen);
            if (useOutputFile) {
                appendJsonlRows(outputFile, tweets);
                outputCount += tweets.length;
            }
            else {
                allTweets.push(...tweets);
            }
            const pageComplete = !nextCursor;
            writeResumeFile(resumeFile, {
                cursor: pageComplete ? null : nextCursor,

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Read the numeric status in the message: 401/403 → re-login to x.com; 429 → wait and retry later.
  2. Re-authenticate the browser session and ensure ct0 cookie is fresh.
  3. Update BOOKMARKS_QUERY_ID / use resolveTwitterQueryId if the endpoint id is stale (query ids rotate with Twitter deployments).
  4. Retry after backoff if rate-limited; reduce request frequency or page count.

Example fix

// before: hammering the API in a tight loop
for (const _ of pages) await fetchBookmarks();
// after: honor 429 with backoff
if (status === 429) await sleep(retryAfterMs);
Defensive patterns

Strategy: retry

Validate before calling

// Preflight: confirm session cookies exist before fetching
const cookies = await page.getCookies({ url: 'https://x.com' });
if (!cookies.some((c) => c.name === 'ct0')) throw new Error('Login to x.com first');

Type guard

function isApiSuccess(data) {
  return data != null && typeof data === 'object' && !('error' in data);
}

Try / catch

try {
  await runBookmarks(argv);
} catch (e) {
  if (e instanceof CommandExecutionError && /Bookmarks.*429|Bookmarks.*401|Bookmarks.*403/.test(e.message)) {
    const status = e.message.match(/(\d{3})/)?.[1];
    if (status === '429') { await sleep(15 * 60_000); return retry(); }
    console.error('Re-authenticate the x.com session and retry.');
  } else throw e;
}

Prevention

When it happens

Trigger: The in-page fetch to /i/api/graphql/<queryId>/Bookmarks returns !r.ok and the fetch loop has produced 0 tweets/output rows so far, i.e. `data.error` is set (e.g. 401/403/429) on the first page.

Common situations: Rate limiting (429) on the first request; expired CSRF/session despite ct0 present (401/403); stale queryId that Twitter no longer accepts; logged-in session lacks bookmarks permission; Twitter API shape/auth change.

Related errors


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