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
- Read the numeric status in the message: 401/403 → re-login to x.com; 429 → wait and retry later.
- Re-authenticate the browser session and ensure ct0 cookie is fresh.
- Update BOOKMARKS_QUERY_ID / use resolveTwitterQueryId if the endpoint id is stale (query ids rotate with Twitter deployments).
- 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
- Throttle requests and respect rate limits; add backoff around archive runs.
- Keep the browser session fresh; re-login before long archives.
- Track Twitter queryId rotations and refresh via resolveTwitterQueryId.
- Surface the numeric HTTP status in monitoring to route 401/403 vs 429 differently.
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.