{"record":{"id":"1ef2f26414f0ef8a","repo":"jackwener/OpenCLI","slug":"twitter-likes-protocol-error","errorCode":"twitter_likes_protocol_error","errorMessage":"twitter_likes_protocol_error: missing Likes timeline instructions","messagePattern":"twitter_likes_protocol_error: missing Likes timeline instructions","errorType":"error_code","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/likes.js","lineNumber":318,"sourceCode":"            const fetchCount = Math.min(100, remaining);\n            const apiUrl = buildLikesUrl(likesQueryId, userId, fetchCount, cursor);\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('Likes', data.error));\n                break;\n            }\n            lastRawResponse = data;\n            const hasInstructions = Array.isArray(data?.data?.user?.result?.timeline_v2?.timeline?.instructions)\n                || Array.isArray(data?.data?.user?.result?.timeline?.timeline?.instructions);\n            if (!hasInstructions) {\n                if (looksLikePrivateTwitterTimeline(data) && (useOutputFile ? outputCount : allTweets.length) === 0) {\n                    throw new EmptyResultError('twitter likes', `No likes returned for @${username} (Likes are private by default on X; only the account owner can view their own likes)`);\n                }\n                throw new CommandExecutionError('twitter_likes_protocol_error: missing Likes timeline instructions');\n            }\n            const { tweets, nextCursor } = parseLikes(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: 'likes',\n                username,","sourceCodeStart":300,"sourceCodeEnd":336,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/likes.js#L300-L336","documentation":"CommandExecutionError (code twitter_likes_protocol_error) thrown when the Likes API responds OK but the payload contains no timeline instructions and does not look like a private-timeline response. This means Twitter returned an unrecognized payload shape, so parsing cannot proceed; the library treats it as a scraping-protocol break rather than an empty result.","triggerScenarios":"After the API call at clis/twitter/likes.js:318, Array.isArray checks on data?.data?.user?.result?.timeline_v2?.timeline?.instructions and the legacy timeline path both fail, and looksLikePrivateTwitterTimeline is false (or some rows were already collected).","commonSituations":"Twitter changed the Likes GraphQL response schema; a stale queryId serving a different response shape; the account returned a guest/limited payload; soft-blocked requests returning ok-with-empty-body responses.","solutions":["Log the raw response (lastRawResponse) and inspect the actual JSON shape.","Re-resolve the Likes queryId with resolveTwitterQueryId to ensure you hit the current GraphQL operation.","Update parseLikes and the instructions paths in likes.js to match Twitter's new schema.","Re-authenticate; a degraded session can yield non-timeline payloads."],"exampleFix":"// before\nconst instructions = data?.data?.user?.result?.timeline_v2?.timeline?.instructions;\n// after\nconst instructions = data?.data?.user?.result?.timeline_v2?.timeline?.instructions\n  || data?.data?.user?.result?.timeline?.timeline?.instructions\n  || data?.data?.user?.result?.timeline?.instructions; // accommodate new/legacy shapes","handlingStrategy":"try-catch","validationCode":"// after a probe request, verify the response shape before the full run\nconst probe = await fetchLikesPage(cursor = null);\nconst ok = Array.isArray(probe?.data?.user?.result?.timeline_v2?.timeline?.instructions)\n  || Array.isArray(probe?.data?.user?.result?.timeline?.timeline?.instructions);\nif (!ok) console.warn('Unexpected Likes payload shape; update parser/queryId.');","typeGuard":"function hasTimelineInstructions(data) {\n  const r = data?.data?.user?.result;\n  return Array.isArray(r?.timeline_v2?.timeline?.instructions)\n    || Array.isArray(r?.timeline?.timeline?.instructions);\n}","tryCatchPattern":"try {\n  await cli.twitter.likes({ username });\n} catch (e) {\n  if (e.code === 'twitter_likes_protocol_error') {\n    console.error('Twitter response schema changed; dump lastRawResponse and update parseLikes/query IDs.');\n  } else throw e;\n}","preventionTips":["Always resolve query IDs dynamically instead of hardcoding","Log lastRawResponse on protocol errors to diagnose schema changes","Keep parser paths tolerant of both timeline_v2 and legacy shapes","Re-authenticate: degraded sessions can return non-timeline payloads"],"tags":["protocol","schema-change","scraping"],"backgroundTag":"schema-changed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}