{"record":{"id":"5639718ca6d4efeb","repo":"jackwener/OpenCLI","slug":"describetwitterapierror-searchtimeline-data-e","errorCode":null,"errorMessage":"${describeTwitterApiError('SearchTimeline', data.error)}","messagePattern":"\\$\\{describeTwitterApiError\\('SearchTimeline', data\\.error\\)\\}","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/search.js","lineNumber":321,"sourceCode":"        // Runaway guard only; --limit and cursor exhaustion control normal pagination.\n        for (let i = 0; i < MAX_PAGINATION_PAGES && results.length < kwargs.limit; i++) {\n            const fetchCount = Number(kwargs.limit) - results.length + 10;\n            const [requestUrl, requestPayload] = buildSearchTimelineRequest(operation, finalQuery, product, fetchCount, cursor);\n            const requestBody = JSON.stringify(requestPayload);\n            const data = normalizeTwitterGraphqlPayload(await page.evaluate(`async () => {\n        const options = {\n          method: 'POST',\n          headers: ${headers},\n          credentials: 'include',\n        };\n        options['body'] = ${JSON.stringify(requestBody)};\n        const r = await fetch(${JSON.stringify(requestUrl)}, {\n          ...options,\n        });\n        return r.ok ? await r.json() : { error: r.status };\n      }`));\n            if (data?.error) {\n                if (results.length === 0) throw new CommandExecutionError(describeTwitterApiError('SearchTimeline', data.error));\n                break;\n            }\n            const { rows, nextCursor } = parseSearchTimeline(data, seen);\n            results.push(...rows);\n            if (!nextCursor || nextCursor === cursor) break;\n            cursor = nextCursor;\n        }\n        const trimmed = results.slice(0, kwargs.limit);\n        return applyTopByEngagement(trimmed, kwargs['top-by-engagement']);\n    }\n});\n\nexport const __test__ = {\n    buildSearchQuery,\n    resolveSearchFParam,\n    resolveSearchProduct,\n    buildSearchTimelineRequest,\n    parseSearchTimeline,","sourceCodeStart":303,"sourceCodeEnd":339,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/search.js#L303-L339","documentation":"CommandExecutionError raised when the SearchTimeline GraphQL response contains an error field and no results were collected. describeTwitterApiError converts X's numeric/structured error (often an HTTP status echoed back as { error: <status> }) into a human-readable message naming the operation.","triggerScenarios":"The in-page fetch to X's SearchTimeline endpoint returns r.ok false (mapped to { error: r.status }) or the JSON body carries data.error — with results.length === 0 so the loop aborts instead of continuing paging. Common values: 401/403 (bad csrf/auth), 429 (rate limit).","commonSituations":"Rate limiting after many rapid searches, expired ct0/bearer mismatch, X changing the SearchTimeline operation ID so the request is rejected, or network/proxy failures inside the page.","solutions":["Check the described status: 401/403 → re-login to refresh ct0; 429 → wait and retry later","Ensure the query passed X's validation (try a simpler query)","Update opencli if X rotated the SearchTimeline operation ID","Retry after a pause; avoid high-frequency searches"],"exampleFix":"// before\nawait opencli.twitter.search(q); // 429 during heavy loop\n// after\nawait sleep(backoffMs);          // wait out rate limit\nawait opencli.twitter.search(q);","handlingStrategy":"retry","validationCode":"// Validate inputs and session before searching\nif (!query.trim()) throw new Error('empty query');\nconst cookies = await page.getCookies({ url: 'https://x.com' });\nif (!cookies.some((c) => c.name === 'ct0')) throw new Error('no session');","typeGuard":"function isTwitterApiError(e) { return e && e.name === 'CommandExecutionError' && /SearchTimeline/.test(e.message); }","tryCatchPattern":"try {\n  return await opencli.twitter.search(q);\n} catch (e) {\n  if (isTwitterApiError(e)) {\n    if (e.message.includes('429')) { await sleep(60000); return retry(); }\n    if (/401|403/.test(e.message)) await refreshXSession();\n    throw e;\n  } else throw e;\n}","preventionTips":["Throttle search frequency to avoid 429 rate limits","Keep sessions fresh (ct0 must match the request csrf)","Update opencli when X rotates the SearchTimeline operation ID","Retry with exponential backoff on transient statuses"],"tags":["twitter","api-error","graphql","rate-limit"],"backgroundTag":"upstream-api-error","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}