jackwener/OpenCLI · error · CommandExecutionError

${describeTwitterApiError('UserTweets', data.error)}

Error message

${describeTwitterApiError('UserTweets', data.error)}

What it means

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.

Source

Thrown at clis/twitter/tweets.js:137

    ],
    columns: ['id', 'author', 'created_at', 'is_retweet', 'text', 'likes', 'retweets', 'replies', 'views', 'url', 'has_media', 'media_urls', 'media_posters', 'quoted_tweet'],
    func: async (page, kwargs) => {
        const limit = normalizeLimit(kwargs.limit);
        const pageDelaySeconds = normalizePageDelaySeconds(kwargs['page-delay']);
        const context = await resolveUserTimelineContext(page, kwargs.username, { allowLoggedInDefault: true });
        const { username } = context;
        const seen = new Set();
        const all = [];
        let cursor = null;
        // Runaway guard only; --limit and cursor exhaustion control normal pagination.
        for (let i = 0; i < MAX_USER_TWEETS_PAGES && all.length < limit; i++) {
            if (i > 0 && pageDelaySeconds > 0) {
                await page.wait(pageDelaySeconds);
            }
            const fetchCount = Math.min(USER_TWEETS_PAGE_SIZE, limit - all.length + 10);
            const data = await fetchUserTimelinePage(page, context, cursor, fetchCount);
            if (data?.error) {
                if (all.length === 0) throw new CommandExecutionError(describeTwitterApiError('UserTweets', data.error));
                break;
            }
            const { tweets, nextCursor } = parseUserTweets(data, seen);
            all.push(...tweets);
            if (!nextCursor || nextCursor === cursor) break;
            cursor = nextCursor;
        }
        if (all.length === 0) throw new EmptyResultError(`@${username} has no recent tweets`, 'Account may be private or suspended');
        return applyTopByEngagement(all.slice(0, limit), kwargs['top-by-engagement']);
    },
});

export const __test__ = {
    MAX_TWEETS_LIMIT,
    sanitizeQueryId,
    buildUserTweetsUrl,
    buildUserByScreenNameUrl,
    extractTweet,

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Read the described status in the message: re-login for 401/403, back off and reduce --page-delay-inverse behavior (increase delays) for 429
  2. Refresh the UserTweets queryId in the codebase from current x.com web app network traffic
  3. Retry the command later if rate-limited; consider a smaller --limit and larger --page-delay
  4. Verify the target handle exists and is not suspended/protected, since first-page errors can also come from such accounts
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight session + sane flags
const cookies = await page.getCookies({ url: 'https://x.com' });
if (!cookies.some((c) => c.name === 'ct0' && c.value)) throw new Error('Login to x.com first');
if (!/^[A-Za-z0-9_]{1,15}$/.test(handle)) throw new Error(`Invalid screen name: ${handle}`);

Type guard

function isUserTweetsPayload(data) {
  return data != null && typeof data === 'object' && !('error' in data);
}

Try / catch

import { CommandExecutionError } from '@jackwener/opencli/errors';
try {
  const tweets = await fetchUserTweets('@jack', { limit: 200, pageDelay: 2 });
} catch (e) {
  if (e instanceof CommandExecutionError && /429/.test(e.message)) {
    await sleep(10 * 60_000); // rate-limited: back off and retry
  } else if (e instanceof CommandExecutionError && /40[134]/.test(e.message)) {
    console.error('Session or UserTweets queryId problem: re-login or update queryId.');
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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