{"record":{"id":"d2155ec3c05da2f8","repo":"jackwener/OpenCLI","slug":"describetwitterapierror-usertweets-data-error","errorCode":null,"errorMessage":"${describeTwitterApiError('UserTweets', data.error)}","messagePattern":"\\$\\{describeTwitterApiError\\('UserTweets', data\\.error\\)\\}","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/tweets.js","lineNumber":137,"sourceCode":"    ],\n    columns: ['id', 'author', 'created_at', 'is_retweet', 'text', 'likes', 'retweets', 'replies', 'views', 'url', 'has_media', 'media_urls', 'media_posters', 'quoted_tweet'],\n    func: async (page, kwargs) => {\n        const limit = normalizeLimit(kwargs.limit);\n        const pageDelaySeconds = normalizePageDelaySeconds(kwargs['page-delay']);\n        const context = await resolveUserTimelineContext(page, kwargs.username, { allowLoggedInDefault: true });\n        const { username } = context;\n        const seen = new Set();\n        const all = [];\n        let cursor = null;\n        // Runaway guard only; --limit and cursor exhaustion control normal pagination.\n        for (let i = 0; i < MAX_USER_TWEETS_PAGES && all.length < limit; i++) {\n            if (i > 0 && pageDelaySeconds > 0) {\n                await page.wait(pageDelaySeconds);\n            }\n            const fetchCount = Math.min(USER_TWEETS_PAGE_SIZE, limit - all.length + 10);\n            const data = await fetchUserTimelinePage(page, context, cursor, fetchCount);\n            if (data?.error) {\n                if (all.length === 0) throw new CommandExecutionError(describeTwitterApiError('UserTweets', data.error));\n                break;\n            }\n            const { tweets, nextCursor } = parseUserTweets(data, seen);\n            all.push(...tweets);\n            if (!nextCursor || nextCursor === cursor) break;\n            cursor = nextCursor;\n        }\n        if (all.length === 0) throw new EmptyResultError(`@${username} has no recent tweets`, 'Account may be private or suspended');\n        return applyTopByEngagement(all.slice(0, limit), kwargs['top-by-engagement']);\n    },\n});\n\nexport const __test__ = {\n    MAX_TWEETS_LIMIT,\n    sanitizeQueryId,\n    buildUserTweetsUrl,\n    buildUserByScreenNameUrl,\n    extractTweet,","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/tweets.js#L119-L155","documentation":"tweets.js:137 fetches a UserTweets page via fetchUserTimelinePage; when the response contains an error field (HTTP error status mapped to { error: status } or a GraphQL error body) and no tweets have been collected yet (all.length === 0), it throws CommandExecutionError with describeTwitterApiError('UserTweets', data.error). Once any page succeeds, later errors merely end pagination with partial results.","triggerScenarios":"The first UserTweets page fetch returns an error payload: 401/403 (session/CSRF rejected), 429 (rate limited), 404 (queryId for UserTweets retired), or a GraphQL error — before any tweet has been parsed.","commonSituations":"Heavy scraping trips rate limits; X rotated the UserTweets queryId so the hard-coded/fallback ID no longer resolves; expired ct0/session; deleted or suspended target account yielding error-shaped responses; schema changes breaking the request FEATURES payload.","solutions":["Read the described status in the message: re-login for 401/403, back off and reduce --page-delay-inverse behavior (increase delays) for 429","Refresh the UserTweets queryId in the codebase from current x.com web app network traffic","Retry the command later if rate-limited; consider a smaller --limit and larger --page-delay","Verify the target handle exists and is not suspended/protected, since first-page errors can also come from such accounts"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Pre-flight session + sane flags\nconst cookies = await page.getCookies({ url: 'https://x.com' });\nif (!cookies.some((c) => c.name === 'ct0' && c.value)) throw new Error('Login to x.com first');\nif (!/^[A-Za-z0-9_]{1,15}$/.test(handle)) throw new Error(`Invalid screen name: ${handle}`);","typeGuard":"function isUserTweetsPayload(data) {\n  return data != null && typeof data === 'object' && !('error' in data);\n}","tryCatchPattern":"import { CommandExecutionError } from '@jackwener/opencli/errors';\ntry {\n  const tweets = await fetchUserTweets('@jack', { limit: 200, pageDelay: 2 });\n} catch (e) {\n  if (e instanceof CommandExecutionError && /429/.test(e.message)) {\n    await sleep(10 * 60_000); // rate-limited: back off and retry\n  } else if (e instanceof CommandExecutionError && /40[134]/.test(e.message)) {\n    console.error('Session or UserTweets queryId problem: re-login or update queryId.');\n  } else throw e;\n}","preventionTips":["Set a generous --page-delay and modest --limit for long user-timeline pulls","Keep the UserTweets queryId and FEATURES in sync with live x.com requests","Pre-verify the target handle exists and is public","Re-authenticate when ct0/session ages out; schedule batch jobs with cooldowns"],"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"}