{"record":{"id":"81b9902087bd5dc0","repo":"jackwener/OpenCLI","slug":"twitter-usermedia-returned-malformed-timeline-inst","errorCode":null,"errorMessage":"Twitter UserMedia returned malformed timeline instructions","messagePattern":"Twitter UserMedia returned malformed timeline instructions","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/download.js","lineNumber":249,"sourceCode":"    }\n    if (result.ok === false) {\n        throwGraphqlFetchError(context, Number(result.status) || 0, typeof result.error === 'string' ? result.error : '');\n    }\n    throw new CommandExecutionError(`Twitter ${context} returned malformed fetch result`);\n}\n\nfunction requireUserMediaPayload(data) {\n    const payload = requireObjectPayload(data, 'UserMedia');\n    if (Array.isArray(payload.errors) && payload.errors.length > 0) {\n        throw new CommandExecutionError(`Twitter UserMedia returned GraphQL errors: ${JSON.stringify(payload.errors).slice(0, 200)}`);\n    }\n    const result = payload.data?.user?.result;\n    if (!result || typeof result !== 'object') {\n        throw new CommandExecutionError('Twitter UserMedia returned malformed user result');\n    }\n    const instructions = result.timeline_v2?.timeline?.instructions || result.timeline?.timeline?.instructions;\n    if (!Array.isArray(instructions)) {\n        throw new CommandExecutionError('Twitter UserMedia returned malformed timeline instructions');\n    }\n    return payload;\n}\n\nfunction parseUserMedia(data, seen) {\n    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;","sourceCodeStart":231,"sourceCodeEnd":267,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/download.js#L231-L267","documentation":"After validating the user result, requireUserMediaPayload expects a timeline instructions array at result.timeline_v2.timeline.instructions or the legacy result.timeline.timeline.instructions path. If neither is an array, it throws 'malformed timeline instructions'. This protects the recursive parser (parseUserMedia/visit) from walking a structure with no entries to visit.","triggerScenarios":"The user result object exists but contains no timeline_v2/timeline instructions — e.g. the account has no media timeline payload, X changed the response schema (timeline_v2 renamed/moved), or the entry has an empty/unexpected node shape.","commonSituations":"X shipping a GraphQL schema change to UserMedia (very common with timeline_v2 vs timeline migrations); new account with no tweets/media returning a different node; A/B-tested response variants for some sessions.","solutions":["Check the latest x.com UserMedia GraphQL response in browser devtools and update the instructions lookup path (timeline_v2 vs timeline) in requireUserMediaPayload.","Re-run after confirming the account actually has media; a truly empty timeline may need an explicit empty-result branch instead of an error.","Verify the queryId matches the current x.com build — mismatched query ids can yield truncated/alternate payloads.","Add support for any new container field X introduced (e.g. additional result.* timeline variants)."],"exampleFix":"// before\nconst instructions = result.timeline_v2?.timeline?.instructions || result.timeline?.timeline?.instructions;\n\n// after: tolerate additional/new container paths\nconst instructions = result.timeline_v2?.timeline?.instructions\n    || result.timeline?.timeline?.instructions\n    || result.timeline_response?.timeline?.instructions;","handlingStrategy":"validation","validationCode":"// Probe the endpoint shape before full parsing\nconst instructions = result?.timeline_v2?.timeline?.instructions ?? result?.timeline?.timeline?.instructions;\nif (!Array.isArray(instructions)) {\n    console.warn('UserMedia timeline shape unrecognized — update parser');\n}","typeGuard":"function hasTimelineInstructions(result) {\n    return Array.isArray(result?.timeline_v2?.timeline?.instructions)\n        || Array.isArray(result?.timeline?.timeline?.instructions);\n}","tryCatchPattern":"try {\n    await twitterDownload(username);\n} catch (err) {\n    if (err instanceof CommandExecutionError && err.message.includes('malformed timeline instructions')) {\n        // pin/upgrade the parser to the current X GraphQL schema, then retry\n    } else throw err;\n}","preventionTips":["Track X GraphQL UserMedia schema changes (timeline_v2 vs timeline paths) regularly","Capture raw responses in devtools to diff new container fields","Pin/verify the GraphQL queryId against the current x.com build","Add an explicit empty-timeline branch so accounts with no media don't error"],"tags":["twitter","graphql","schema-change","api-response"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}