{"record":{"id":"9612cb69fcf8029f","repo":"jackwener/OpenCLI","slug":"twitter-usermedia-returned-a-tweet-without-rest-id","errorCode":null,"errorMessage":"Twitter UserMedia returned a tweet without rest_id","messagePattern":"Twitter UserMedia returned a tweet without rest_id","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/download.js","lineNumber":273,"sourceCode":"    const items = [];\n    let nextCursor = null;\n    const result = requireUserMediaPayload(data).data.user.result;\n    const instructionSets = [\n        result.timeline_v2?.timeline?.instructions,\n        result.timeline?.timeline?.instructions,\n    ].filter(Array.isArray);\n    const instructions = instructionSets.flat();\n    const visit = (value) => {\n        if (!value || typeof value !== 'object') return;\n        if (value.type === 'TimelinePinEntry') return;\n        if (value.tweet_results?.result) {\n            const raw = value.tweet_results.result;\n            const tw = raw.__typename === 'TweetWithVisibilityResults' && raw.tweet\n                ? raw.tweet\n                : (raw.tweet || raw);\n            const tweetId = typeof tw.rest_id === 'string' || typeof tw.rest_id === 'number' ? String(tw.rest_id) : '';\n            if (!tweetId) {\n                throw new CommandExecutionError('Twitter UserMedia returned a tweet without rest_id');\n            }\n            if (!seen.has(tweetId)) {\n                seen.add(tweetId);\n                const { media_urls } = extractMedia(tw.legacy || {});\n                for (const url of media_urls) {\n                    items.push({ tweet_id: tweetId, url, type: classifyMediaUrl(url) });\n                }\n            }\n        }\n        if (\n            (value.entryType === 'TimelineTimelineCursor' || value.__typename === 'TimelineTimelineCursor')\n            && (value.cursorType === 'Bottom' || value.cursorType === 'ShowMore')\n            && value.value\n        ) {\n            nextCursor = value.value;\n        }\n        if (Array.isArray(value)) {\n            for (const item of value) visit(item);","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/download.js#L255-L291","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Update the unwrap logic in visit() to handle new __typename variants that carry rest_id in a nested field.","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.","Re-run with an authenticated session that can see the gated content, if the missing rest_id stems from age/sensitive-content gating.","Log the offending node's __typename to confirm which variant is appearing and extend parsing for it."],"exampleFix":"// before\nif (!tweetId) {\n    throw new CommandExecutionError('Twitter UserMedia returned a tweet without rest_id');\n}\n\n// after: skip tombstoned entries instead of failing the whole scan\nif (!tweetId) {\n    return; // or log-and-continue in visit()\n}","handlingStrategy":"type-guard","validationCode":"// Pre-filter nodes lacking rest_id before strict parsing\nconst usable = node?.tweet_results?.result\n    && typeof (node.tweet_results.result.tweet ?? node.tweet_results.result).rest_id !== 'undefined';","typeGuard":"function hasRestId(raw) {\n    const tw = raw?.__typename === 'TweetWithVisibilityResults' && raw.tweet ? raw.tweet : (raw?.tweet || raw);\n    return typeof tw?.rest_id === 'string' || typeof tw?.rest_id === 'number';\n}","tryCatchPattern":"try {\n    await twitterDownload(username);\n} catch (err) {\n    if (err instanceof CommandExecutionError && err.message.includes('without rest_id')) {\n        // degrade gracefully: skip unparseable tweet nodes and continue the scan\n    } else throw err;\n}","preventionTips":["Handle TweetWithVisibilityResults and any new __typename variants in unwrapping","Expect tombstoned/withheld/age-gated tweets on large profiles and skip them","Use a fully authenticated session so gated content resolves to real tweet nodes","Log the node __typename when rest_id is missing to spot new schema variants"],"tags":["twitter","graphql","parsing","scraping"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}