jackwener/OpenCLI · error · CommandExecutionError

Twitter UserMedia returned a tweet without rest_id

Error message

Twitter UserMedia returned a tweet without rest_id

What it means

parseUserMedia recursively walks timeline instructions; when it finds a tweet_results.result node it unwraps TweetWithVisibilityResults and requires the resulting tweet object to have a rest_id (string or number). If the tweet node exists but has no rest_id, it throws this error instead of silently skipping the entry. This indicates a tweet-shaped node the parser cannot key on.

Source

Thrown at clis/twitter/download.js:273

    const items = [];
    let nextCursor = null;
    const result = requireUserMediaPayload(data).data.user.result;
    const instructionSets = [
        result.timeline_v2?.timeline?.instructions,
        result.timeline?.timeline?.instructions,
    ].filter(Array.isArray);
    const instructions = instructionSets.flat();
    const visit = (value) => {
        if (!value || typeof value !== 'object') return;
        if (value.type === 'TimelinePinEntry') return;
        if (value.tweet_results?.result) {
            const raw = value.tweet_results.result;
            const tw = raw.__typename === 'TweetWithVisibilityResults' && raw.tweet
                ? raw.tweet
                : (raw.tweet || raw);
            const tweetId = typeof tw.rest_id === 'string' || typeof tw.rest_id === 'number' ? String(tw.rest_id) : '';
            if (!tweetId) {
                throw new CommandExecutionError('Twitter UserMedia returned a tweet without rest_id');
            }
            if (!seen.has(tweetId)) {
                seen.add(tweetId);
                const { media_urls } = extractMedia(tw.legacy || {});
                for (const url of media_urls) {
                    items.push({ tweet_id: tweetId, url, type: classifyMediaUrl(url) });
                }
            }
        }
        if (
            (value.entryType === 'TimelineTimelineCursor' || value.__typename === 'TimelineTimelineCursor')
            && (value.cursorType === 'Bottom' || value.cursorType === 'ShowMore')
            && value.value
        ) {
            nextCursor = value.value;
        }
        if (Array.isArray(value)) {
            for (const item of value) visit(item);

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Update the unwrap logic in visit() to handle new __typename variants that carry rest_id in a nested field.
  2. Skip (rather than throw) tweet nodes without rest_id if tombstones should not abort a full profile download — wrap the tweetId check in a continue.
  3. Re-run with an authenticated session that can see the gated content, if the missing rest_id stems from age/sensitive-content gating.
  4. Log the offending node's __typename to confirm which variant is appearing and extend parsing for it.

Example fix

// before
if (!tweetId) {
    throw new CommandExecutionError('Twitter UserMedia returned a tweet without rest_id');
}

// after: skip tombstoned entries instead of failing the whole scan
if (!tweetId) {
    return; // or log-and-continue in visit()
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Pre-filter nodes lacking rest_id before strict parsing
const usable = node?.tweet_results?.result
    && typeof (node.tweet_results.result.tweet ?? node.tweet_results.result).rest_id !== 'undefined';

Type guard

function hasRestId(raw) {
    const tw = raw?.__typename === 'TweetWithVisibilityResults' && raw.tweet ? raw.tweet : (raw?.tweet || raw);
    return typeof tw?.rest_id === 'string' || typeof tw?.rest_id === 'number';
}

Try / catch

try {
    await twitterDownload(username);
} catch (err) {
    if (err instanceof CommandExecutionError && err.message.includes('without rest_id')) {
        // degrade gracefully: skip unparseable tweet nodes and continue the scan
    } else throw err;
}

Prevention

When it happens

Trigger: A tweet_results.result node in the UserMedia timeline lacks rest_id — e.g. tombstoned/deleted tweets, withheld or age-gated content nodes, availability-limited entries, or a new __typename variant not handled by the TweetWithVisibilityResults unwrap.

Common situations: Scraping a profile containing deleted/tombstoned tweets; tweets withheld in your region or behind sensitive-content gating; X introducing new result __typename shapes; inconsistent payloads for unauthenticated/limited sessions.

Related errors


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