{"record":{"id":"ccb975dd2f7b1b1e","repo":"jackwener/OpenCLI","slug":"likes","errorCode":null,"errorMessage":"Likes","messagePattern":"Likes","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/likes.js","lineNumber":308,"sourceCode":"        let outputCount = useOutputFile ? jsonlState.count : 0;\n        let cursor = resumed?.cursor || null;\n        let lastRawResponse = null;\n        let pages = 0;\n        let exhausted = false;\n        // Runaway guard only; --limit/--all and cursor exhaustion control normal pagination.\n        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 = 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);","sourceCodeStart":290,"sourceCodeEnd":326,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/likes.js#L290-L326","documentation":"CommandExecutionError produced by describeTwitterApiError('Likes', data.error) when the first Likes GraphQL request fails outright (HTTP error status returned inside the page fetch) and nothing has been collected yet. The library surfaces the API's error status/code through the shared describer so the user sees the underlying Twitter API failure.","triggerScenarios":"The in-page fetch of the Likes GraphQL endpoint returns r.ok === false (data = { error: r.status }), and the run has zero collected likes so far (useOutputFile ? outputCount : allTweets.length === 0) at clis/twitter/likes.js:308.","commonSituations":"Expired auth/ct0 token giving 401/403; Twitter rate limiting (429); stale hardcoded LIKES_QUERY_ID after Twitter rotated GraphQL query IDs (400); Cloudflare/anomaly challenge pages.","solutions":["Re-authenticate the x.com session (log in again) to refresh bearer/csrf tokens.","Wait and retry if rate limited (429).","Update LIKES_QUERY_ID (and FEATURES) if Twitter rotated the GraphQL query ID, by extracting the current ID from x.com's main bundle via resolveTwitterQueryId.","Check describeTwitterApiError output for the exact HTTP status and address it (401->login, 403->permissions/protected account)."],"exampleFix":"// before\nconst LIKES_QUERY_ID = 'CDWHmpZeSdIJ3HGeRbNm0w'; // stale, 400 from API\n// after\nconst LIKES_QUERY_ID = await resolveTwitterQueryId(page, 'Likes', 'CDWHmpZeSdIJ3HGeRbNm0w'); // resolve fresh from x.com bundle","handlingStrategy":"retry","validationCode":"// preflight: verify the session can reach the GraphQL endpoint\nconst probe = await page.evaluate(() => fetch('/i/api/graphql/.../Likes?...', { credentials: 'include' }).then(r => r.status));\nif (probe === 401 || probe === 403) await reauthenticate();","typeGuard":"null","tryCatchPattern":"try {\n  await cli.twitter.likes({ username });\n} catch (e) {\n  if (/Likes.*error|describeTwitterApiError/.test(e.constructor.name + e.message) && e.status === 429) {\n    await sleep(backoff);\n    return retry();\n  }\n  if (e.status === 401) await reauthenticate();\n  throw e;\n}","preventionTips":["Refresh the x.com session regularly during long fetches","Respect rate limits; add delays between pages","Re-resolve GraphQL query IDs when Twitter rotates them","Surface the described HTTP status (401/403/429) to decide the fix"],"tags":["api","http-error","twitter","rate-limit"],"backgroundTag":"twitter-api-error","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}