{"record":{"id":"ca1bd2c73c4121a3","repo":"jackwener/OpenCLI","slug":"describetwitterapierror-tweetdetail-data-erro","errorCode":null,"errorMessage":"${describeTwitterApiError('TweetDetail', data.error)}","messagePattern":"\\$\\{describeTwitterApiError\\('TweetDetail', data\\.error\\)\\}","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/thread.js","lineNumber":151,"sourceCode":"            'X-Twitter-Auth-Type': 'OAuth2Session',\n            'X-Twitter-Active-User': 'yes',\n        });\n        // Paginate — fetch in browser, parse in TypeScript\n        const allTweets = [];\n        const seen = new Set();\n        let cursor = null;\n        for (let i = 0; i < 5; i++) {\n            const apiUrl = buildTweetDetailUrl(tweetId, cursor);\n            // Browser-side: fetch + JSON parse with HTML-as-JSON sniffer so a\n            // login wall / WAF page surfaces as a structured LoginWallError\n            // instead of `SyntaxError: Unexpected token '<'`.\n            const data = throwIfLoginWall(await page.evaluate(`async () => {\n        ${BROWSER_JSON_SNIFF_FN}\n        return await fetchJsonOrLoginWall(\"${apiUrl}\", { headers: ${headers}, credentials: 'include' });\n      }`), { url: apiUrl });\n            if (data?.error) {\n                if (allTweets.length === 0)\n                    throw new CommandExecutionError(describeTwitterApiError('TweetDetail', data.error));\n                break;\n            }\n            // TypeScript-side: type-safe parsing + cursor extraction\n            const { tweets, nextCursor } = parseTweetDetail(data, seen);\n            allTweets.push(...tweets);\n            if (!nextCursor || nextCursor === cursor)\n                break;\n            cursor = nextCursor;\n        }\n        const trimmed = allTweets.slice(0, kwargs.limit);\n        return applyTopByEngagement(trimmed, kwargs['top-by-engagement']);\n    },\n});\n","sourceCodeStart":133,"sourceCodeEnd":165,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/thread.js#L133-L165","documentation":"thread.js:151 wraps the TweetDetail GraphQL response with throwIfLoginWall and then checks data.error. If the API returned an error payload and no tweets have been collected yet (allTweets.length === 0), it throws CommandExecutionError with a human-readable description from describeTwitterApiError('TweetDetail', data.error). If some tweets were already fetched, it breaks out of pagination instead of throwing.","triggerScenarios":"The TweetDetail fetch inside page.evaluate returns { error: <status|message> } — typically an HTTP error status from fetchJsonOrLoginWall (401/403 auth rejection, 429 rate limit, 404 stale queryId), or a GraphQL error body — on the first page of results.","commonSituations":"Rate limiting after heavy scraping; X changed the GraphQL response shape or retired the hard-coded TWEET_DETAIL_QUERY_ID; session/CSRF token rejected mid-run; deleted or protected tweet where the API returns errors instead of an empty timeline; login wall detected on the first fetch.","solutions":["Read the described error in the message (e.g. status 429 vs 403) and address it: wait out rate limits or re-login for auth errors","Retry later or with fewer requests if it is a rate-limit/429 case; the API is throttling the session","Update the hard-coded TWEET_DETAIL_QUERY_ID / FEATURES in thread.js to the current values x.com uses (sniff them from the web app's network tab)","Confirm the tweet still exists and is not protected/deleted — those can produce error payloads on first fetch"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Pre-flight: confirm session still valid with a cheap authenticated endpoint\nconst cookies = await page.getCookies({ url: 'https://x.com' });\nconst ct0 = cookies.find((c) => c.name === 'ct0')?.value;\nif (!ct0) throw new Error('Login required before calling thread fetch');","typeGuard":"function isApiErrorPayload(data) {\n  return data != null && typeof data === 'object' && 'error' in data;\n}","tryCatchPattern":"import { CommandExecutionError } from '@jackwener/opencli/errors';\ntry {\n  const tweets = await fetchThread(url);\n} catch (e) {\n  if (e instanceof CommandExecutionError && /429|rate/i.test(e.message)) {\n    await sleep(60_000);\n    return fetchThread(url); // retry with backoff\n  }\n  if (e instanceof CommandExecutionError && /40[134]/.test(e.message)) {\n    console.error('Session/queryId issue: re-login or update TWEET_DETAIL_QUERY_ID.');\n  } else throw e;\n}","preventionTips":["Throttle requests and add delays between thread fetches to avoid 429s","Pin and periodically refresh the TweetDetail queryId and FEATURES from live x.com traffic","Log the raw error status from the API payload to distinguish auth vs rate-limit vs not-found","Cache fetched threads to reduce repeat calls"],"tags":["api-error","rate-limit","twitter","graphql"],"backgroundTag":"upstream-api-error","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}