{"record":{"id":"09fada712c4be405","repo":"jackwener/OpenCLI","slug":"twitter-bookmarks-protocol-error","errorCode":"twitter_bookmarks_protocol_error","errorMessage":"twitter_bookmarks_protocol_error: missing Bookmarks timeline instructions","messagePattern":"twitter_bookmarks_protocol_error: missing Bookmarks timeline instructions","errorType":"error_code","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/bookmarks.js","lineNumber":256,"sourceCode":"        while (pages < maxPages && (fetchAll || allTweets.length < limit)) {\n            pages += 1;\n            const currentCount = useOutputFile ? outputCount : allTweets.length;\n            const remaining = fetchAll ? 100 : (limit - currentCount + 10);\n            const fetchCount = Math.min(100, remaining);\n            const apiUrl = buildBookmarksUrl(fetchCount, cursor).replace(BOOKMARKS_QUERY_ID, queryId);\n            const data = unwrapBrowserResult(await page.evaluate(`async () => {\n        const r = await fetch(${JSON.stringify(apiUrl)}, { headers: ${headers}, credentials: 'include' });\n        return r.ok ? await r.json() : { error: r.status };\n      }`));\n            if (data?.error) {\n                if ((useOutputFile ? outputCount : allTweets.length) === 0)\n                    throw new CommandExecutionError(describeTwitterApiError('Bookmarks', data.error));\n                break;\n            }\n            const hasInstructions = Array.isArray(data?.data?.bookmark_timeline_v2?.timeline?.instructions)\n                || Array.isArray(data?.data?.bookmark_timeline?.timeline?.instructions);\n            if (!hasInstructions) {\n                throw new CommandExecutionError('twitter_bookmarks_protocol_error: missing Bookmarks timeline instructions');\n            }\n            const { tweets, nextCursor } = parseBookmarks(data, seen);\n            if (useOutputFile) {\n                appendJsonlRows(outputFile, tweets);\n                outputCount += tweets.length;\n            }\n            else {\n                allTweets.push(...tweets);\n            }\n            const pageComplete = !nextCursor;\n            writeResumeFile(resumeFile, {\n                cursor: pageComplete ? null : nextCursor,\n                count: useOutputFile ? outputCount : allTweets.length,\n                tweets: useOutputFile ? undefined : allTweets,\n                updatedAt: new Date().toISOString(),\n                complete: pageComplete,\n                source: 'bookmarks',\n                outputFile: useOutputFile ? outputFile : null,","sourceCodeStart":238,"sourceCodeEnd":274,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/bookmarks.js#L238-L274","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst hasInstructions = Array.isArray(data?.data?.bookmark_timeline_v2?.timeline?.instructions)\n  || Array.isArray(data?.data?.bookmark_timeline?.timeline?.instructions);\n// after: also accept the newer shape once confirmed\nconst hasInstructions = Array.isArray(data?.data?.bookmark_timeline_v2?.timeline?.instructions)\n  || Array.isArray(data?.data?.bookmark_timeline?.timeline?.instructions)\n  || Array.isArray(data?.data?.bookmark_timeline_v2?.instructions);","handlingStrategy":"type-guard","validationCode":"// Log one raw response and assert the expected shape before full runs\nconst data = await fetchFirstPage();\nif (!data?.data?.bookmark_timeline_v2?.timeline?.instructions\n  && !data?.data?.bookmark_timeline?.timeline?.instructions) {\n  console.warn('Response schema changed — inspect payload before archiving');\n}","typeGuard":"function hasBookmarksTimeline(data) {\n  const d = data?.data;\n  return Array.isArray(d?.bookmark_timeline_v2?.timeline?.instructions)\n    || Array.isArray(d?.bookmark_timeline?.timeline?.instructions);\n}","tryCatchPattern":"try {\n  await runBookmarks(argv);\n} catch (e) {\n  if (e instanceof CommandExecutionError && e.message.includes('twitter_bookmarks_protocol_error')) {\n    console.error('Bookmarks API payload missing timeline instructions. Verify queryId freshness and schema; inspect raw response.');\n    process.exitCode = 1;\n  } else throw e;\n}","preventionTips":["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."],"tags":["twitter","protocol","schema-change","api"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}