jackwener/OpenCLI · error · CommandExecutionError
twitter_bookmarks_protocol_error
twitter_bookmarks_protocol_error
Error message
twitter_bookmarks_protocol_error: missing Bookmarks timeline instructions
What it means
The Bookmarks API responded with HTTP 200 but its JSON body contains neither data.bookmark_timeline_v2.timeline.instructions nor data.bookmark_timeline.timeline.instructions, so no timeline can be parsed. clis/twitter/bookmarks.js:256 throws CommandExecutionError tagged twitter_bookmarks_protocol_error, guarding against silently treating a malformed payload as an empty timeline.
Source
Thrown at clis/twitter/bookmarks.js:256
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,
count: useOutputFile ? outputCount : allTweets.length,
tweets: useOutputFile ? undefined : allTweets,
updatedAt: new Date().toISOString(),
complete: pageComplete,
source: 'bookmarks',
outputFile: useOutputFile ? outputFile : null,View on GitHub (pinned to 49907e53dc)
Solutions
- Log the raw response body and compare against the expected instructions schema to identify the new shape.
- Refresh BOOKMARKS_QUERY_ID via resolveTwitterQueryId(page, 'Bookmarks', ...) — a stale query id often yields mismatched payloads.
- Verify the session is still valid; a 200 body without timeline instructions can indicate soft auth failure.
- Update parseBookmarks and the hasInstructions check for the new API schema after confirming via browser devtools.
Example fix
// before const hasInstructions = Array.isArray(data?.data?.bookmark_timeline_v2?.timeline?.instructions) || Array.isArray(data?.data?.bookmark_timeline?.timeline?.instructions); // after: also accept the newer shape once confirmed const hasInstructions = Array.isArray(data?.data?.bookmark_timeline_v2?.timeline?.instructions) || Array.isArray(data?.data?.bookmark_timeline?.timeline?.instructions) || Array.isArray(data?.data?.bookmark_timeline_v2?.instructions);
Defensive patterns
Strategy: type-guard
Validate before calling
// Log one raw response and assert the expected shape before full runs
const data = await fetchFirstPage();
if (!data?.data?.bookmark_timeline_v2?.timeline?.instructions
&& !data?.data?.bookmark_timeline?.timeline?.instructions) {
console.warn('Response schema changed — inspect payload before archiving');
} Type guard
function hasBookmarksTimeline(data) {
const d = data?.data;
return Array.isArray(d?.bookmark_timeline_v2?.timeline?.instructions)
|| Array.isArray(d?.bookmark_timeline?.timeline?.instructions);
} Try / catch
try {
await runBookmarks(argv);
} catch (e) {
if (e instanceof CommandExecutionError && e.message.includes('twitter_bookmarks_protocol_error')) {
console.error('Bookmarks API payload missing timeline instructions. Verify queryId freshness and schema; inspect raw response.');
process.exitCode = 1;
} else throw e;
} Prevention
- Resolve the query id dynamically (resolveTwitterQueryId) instead of hardcoding it.
- Smoke-test one page and validate the schema before long archive runs.
- Log raw payloads (to a debug file) to make future schema changes diagnosable.
- Watch for 200 responses containing HTML or error envelopes after Twitter deployments.
When it happens
Trigger: data.data lacks both bookmark_timeline_v2 and bookmark_timeline instruction arrays in a successful (r.ok) response — detected after JSON parsing of the first successful page.
Common situations: Twitter changed the GraphQL response schema or renamed timeline keys; the hardcoded BOOKMARKS_QUERY_ID now maps to a different endpoint returning a different shape; account restrictions return 200 with an error envelope; a WAF/login HTML page served with 200 status.
Related errors
- twitter_collection_protocol_error
- Cannot resolve aid for bvid: ${bvid}
- Bilibili reply add API did not return rpid for the posted co
- pubmed article ${pmid} did not include a title
- pubmed author did not return an id list
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/09fada712c4be405.
Report an issue: GitHub.