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
- Read the described status in the message: re-login for 401/403, back off and reduce --page-delay-inverse behavior (increase delays) for 429
- Refresh the UserTweets queryId in the codebase from current x.com web app network traffic
- Retry the command later if rate-limited; consider a smaller --limit and larger --page-delay
- 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
- Set a generous --page-delay and modest --limit for long user-timeline pulls
- Keep the UserTweets queryId and FEATURES in sync with live x.com requests
- Pre-verify the target handle exists and is public
- Re-authenticate when ct0/session ages out; schedule batch jobs with cooldowns
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
- twitter_collection_request_error
- ${describeTwitterApiError('SearchTimeline', data.error)}
- ${describeTwitterApiError('TweetDetail', data.error)}
- ${describeTwitterApiError(endpoint, data.error)}
- ${probe.detail}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/d2155ec3c05da2f8.
Report an issue: GitHub.