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

  1. Check the described status: 401/403 → re-login to refresh ct0; 429 → wait and retry later
  2. Ensure the query passed X's validation (try a simpler query)
  3. Update opencli if X rotated the SearchTimeline operation ID
  4. 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

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


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/5639718ca6d4efeb. Report an issue: GitHub.