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

  1. Log the raw response body and compare against the expected instructions schema to identify the new shape.
  2. Refresh BOOKMARKS_QUERY_ID via resolveTwitterQueryId(page, 'Bookmarks', ...) — a stale query id often yields mismatched payloads.
  3. Verify the session is still valid; a 200 body without timeline instructions can indicate soft auth failure.
  4. 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

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


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