jackwener/OpenCLI · error · CommandExecutionError
${describeTwitterApiError('SearchTimeline', data.error)}
Error message
${describeTwitterApiError('SearchTimeline', data.error)} What it means
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.
Source
Thrown at clis/twitter/search.js:321
// Runaway guard only; --limit and cursor exhaustion control normal pagination.
for (let i = 0; i < MAX_PAGINATION_PAGES && results.length < kwargs.limit; i++) {
const fetchCount = Number(kwargs.limit) - results.length + 10;
const [requestUrl, requestPayload] = buildSearchTimelineRequest(operation, finalQuery, product, fetchCount, cursor);
const requestBody = JSON.stringify(requestPayload);
const data = normalizeTwitterGraphqlPayload(await page.evaluate(`async () => {
const options = {
method: 'POST',
headers: ${headers},
credentials: 'include',
};
options['body'] = ${JSON.stringify(requestBody)};
const r = await fetch(${JSON.stringify(requestUrl)}, {
...options,
});
return r.ok ? await r.json() : { error: r.status };
}`));
if (data?.error) {
if (results.length === 0) throw new CommandExecutionError(describeTwitterApiError('SearchTimeline', data.error));
break;
}
const { rows, nextCursor } = parseSearchTimeline(data, seen);
results.push(...rows);
if (!nextCursor || nextCursor === cursor) break;
cursor = nextCursor;
}
const trimmed = results.slice(0, kwargs.limit);
return applyTopByEngagement(trimmed, kwargs['top-by-engagement']);
}
});
export const __test__ = {
buildSearchQuery,
resolveSearchFParam,
resolveSearchProduct,
buildSearchTimelineRequest,
parseSearchTimeline,View on GitHub (pinned to 49907e53dc)
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
Example fix
// before await opencli.twitter.search(q); // 429 during heavy loop // after await sleep(backoffMs); // wait out rate limit await opencli.twitter.search(q);
Defensive patterns
Strategy: retry
Validate before calling
// Validate inputs and session before searching
if (!query.trim()) throw new Error('empty query');
const cookies = await page.getCookies({ url: 'https://x.com' });
if (!cookies.some((c) => c.name === 'ct0')) throw new Error('no session'); Type guard
function isTwitterApiError(e) { return e && e.name === 'CommandExecutionError' && /SearchTimeline/.test(e.message); } Try / catch
try {
return await opencli.twitter.search(q);
} catch (e) {
if (isTwitterApiError(e)) {
if (e.message.includes('429')) { await sleep(60000); return retry(); }
if (/401|403/.test(e.message)) await refreshXSession();
throw e;
} else throw e;
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- twitter_collection_request_error
- ${describeTwitterApiError('TweetDetail', data.error)}
- ${describeTwitterApiError(endpoint, data.error)}
- ${describeTwitterApiError('UserTweets', data.error)}
- ${probe.detail}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/5639718ca6d4efeb.
Report an issue: GitHub.