{"record":{"id":"458d949761bb0f55","repo":"jackwener/OpenCLI","slug":"describetwitterapierror-endpoint-data-error","errorCode":null,"errorMessage":"${describeTwitterApiError(endpoint, data.error)}","messagePattern":"\\$\\{describeTwitterApiError\\(endpoint, data\\.error\\)\\}","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/timeline.js","lineNumber":194,"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        // Runaway guard only; --limit and cursor exhaustion control normal pagination.\n        for (let i = 0; i < MAX_PAGINATION_PAGES && allTweets.length < limit; i++) {\n            const fetchCount = Math.min(40, limit - allTweets.length + 5); // over-fetch slightly for promoted filtering\n            const variables = buildTimelineVariables(timelineType, fetchCount, cursor);\n            const apiUrl = buildHomeTimelineUrl(queryId, endpoint, variables);\n            const data = await page.evaluate(`async () => {\n        const r = await fetch(\"${apiUrl}\", { method: \"${method}\", headers: ${headers}, credentials: 'include' });\n        return r.ok ? await r.json() : { error: r.status };\n      }`);\n            if (data?.error) {\n                if (allTweets.length === 0)\n                    throw new CommandExecutionError(describeTwitterApiError(endpoint, data.error));\n                break;\n            }\n            const { tweets, nextCursor } = parseHomeTimeline(data, seen);\n            allTweets.push(...tweets);\n            if (!nextCursor || nextCursor === cursor)\n                break;\n            cursor = nextCursor;\n        }\n        const trimmed = allTweets.slice(0, limit);\n        return applyTopByEngagement(trimmed, kwargs['top-by-engagement']);\n    },\n});\nexport const __test__ = {\n    buildTimelineVariables,\n    buildHomeTimelineUrl,\n    parseHomeTimeline,\n};\n","sourceCodeStart":176,"sourceCodeEnd":212,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/timeline.js#L176-L212","documentation":"timeline.js:194 fetches a timeline page inside page.evaluate and converts non-OK responses to { error: r.status }. If data.error is present and no tweets were collected yet (allTweets.length === 0), it throws CommandExecutionError described by describeTwitterApiError(endpoint, data.error); otherwise pagination just stops with what was gathered.","triggerScenarios":"The timeline fetch returns a non-OK HTTP status (401/403 session rejected, 429 rate limited, 404 queryId retired) or a GraphQL error body on the first page of the selected TIMELINE_ENDPOINTS entry.","commonSituations":"Aggressive polling tripping x.com rate limits; stale/rotated queryId for the endpoint; expired auth mid-run; X changing response schema so parsing fails upstream and error shapes surface; protected/private accounts in UserTweets-like endpoints.","solutions":["Inspect the status code in the message: 429 → back off and slow down requests; 401/403 → re-login to refresh ct0/session","Reduce request frequency and add page delays between timeline pages","Update the endpoint's queryId (fallbackQueryId in TIMELINE_ENDPOINTS) to the current value from x.com's web app network traffic","If errors appear only after some pages, rely on the partial results — the command breaks instead of throwing once tweets exist"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Ensure session before paginating\nconst cookies = await page.getCookies({ url: 'https://x.com' });\nif (!cookies.some((c) => c.name === 'ct0' && c.value)) throw new Error('Login required');\n// Validate the flag combo up front\nif (!Number.isInteger(limit) || limit < 1) throw new Error('limit must be a positive integer');","typeGuard":"function hasTimelineData(data) {\n  return data != null && typeof data === 'object' && !('error' in data)\n    && Array.isArray(data?.data?.home?.home_timeline_urt?.instructions ?? []);\n}","tryCatchPattern":"import { CommandExecutionError } from '@jackwener/opencli/errors';\ntry {\n  const tweets = await fetchTimeline('home', { limit: 100 });\n} catch (e) {\n  if (e instanceof CommandExecutionError && /429/.test(e.message)) {\n    await sleep(5 * 60_000); // back off on rate limit, then retry\n  } else if (e instanceof CommandExecutionError) {\n    console.error(`Timeline API failed (${e.message}); check session or queryId.`);\n  } else throw e;\n}","preventionTips":["Increase --page-delay and reduce --limit to stay under rate limits","Refresh endpoint queryIds when X rotates them","Persist partial results so a mid-run failure doesn't lose data","Monitor for 401/403 and re-login proactively before long jobs"],"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-29T12:17:43.993Z"}