{"record":{"id":"29ef26838e8bfb87","repo":"jackwener/OpenCLI","slug":"twitter-image-upload","errorCode":null,"errorMessage":"twitter image upload","messagePattern":"twitter image upload","errorType":"exception","errorClass":"TimeoutError","httpStatus":null,"severity":"error","filePath":"clis/twitter/post.js","lineNumber":335,"sourceCode":"        // Attach media before inserting text. Uploading media after Draft.js has\n        // text can re-render/reset the editor, causing image-only posts.\n        if (absPaths.length > 0) {\n            await page.wait({ selector: FILE_INPUT_SELECTOR, timeout: 20 });\n            if (page.setFileInput) {\n                try {\n                    await page.setFileInput(absPaths, FILE_INPUT_SELECTOR);\n                } catch (err) {\n                    if (!isRecoverableFileInputError(err)) {\n                        throw err;\n                    }\n                    await attachImagesViaDataTransfer(page, absPaths);\n                }\n            } else {\n                await attachImagesViaDataTransfer(page, absPaths);\n            }\n            const uploadState = await waitForImageUpload(page, absPaths.length);\n            if (!uploadState?.ok) {\n                throw new TimeoutError('twitter image upload', UPLOAD_TIMEOUT_MS / 1000, 'Nothing was posted. Retry, or attach a smaller image.');\n            }\n        }\n\n        // Insert and verify the text after media upload so text + images are in\n        // the final Draft.js composer state immediately before clicking Post.\n        const typeResult = await insertComposerText(page, text);\n        if (!typeResult?.ok) {\n            throw new CommandExecutionError(typeResult?.message ?? 'Could not type tweet text.', 'Open the composer in the browser and check whether X is asking you to log in.');\n        }\n\n        await page.wait(1);\n        const result = await submitTweet(page, text);\n        if (result?.unconfirmed) {\n            // The poll expiring does not mean the tweet stayed in the composer,\n            // so this must not read as a definite failure: the agent workflow\n            // retries CommandExecutionError and would post twice (#2255).\n            throw new TimeoutError('twitter post', SUBMIT_TIMEOUT_MS / 1000, `${result.message} Check \\`opencli twitter tweets --limit 1\\` before retrying; the post may already be live.`);\n        }","sourceCodeStart":317,"sourceCodeEnd":353,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/post.js#L317-L353","documentation":"TimeoutError thrown when the images attached to a tweet composer never finish uploading within UPLOAD_TIMEOUT_MS. waitForImageUpload polls the composer until the expected number of uploaded media thumbnails appear; if it can't confirm them, the command aborts with a message noting nothing was posted so users know a retry is safe.","triggerScenarios":"Calling `opencli twitter post` with --images where setFileInput or the DataTransfer fallback attached files but the composer never shows the expected number of uploaded thumbnails before the timeout: slow network, oversized image, unsupported format slipping through, or X's media pipeline failing silently.","commonSituations":"Uploading a multi-MB photo on a slow connection; attaching a 4-image set where one fails; X A/B UI changes hiding the upload progress indicator; animated GIFs that X re-encodes slowly.","solutions":["Retry the command; transient upload slowness is the most common cause","Attach smaller or fewer images (compress to under a few MB)","Verify the image formats are jpg/png/gif/webp and readable by the browser session","Check the browser session is healthy and logged in; re-run `opencli login` if the composer looked broken","Pin/report if X changed the composer DOM so waitForImageUpload's selectors no longer match"],"exampleFix":"// before\nawait opencli('twitter post', { text: 'hi', images: 'huge-20mb.png' });\n// after\nconst small = await compress('huge-20mb.png', { maxWidth: 1600 });\nawait opencli('twitter post', { text: 'hi', images: small });","handlingStrategy":"retry","validationCode":"const files = images.split(',').map(s => s.trim());\nconst stat = await fs.stat(f);\nif (!/\\.(jpe?g|png|gif|webp)$/i.test(f)) throw new Error('unsupported format: ' + f);\nif (stat.size > 5 * 1024 * 1024) throw new Error('image too large: ' + f);","typeGuard":null,"tryCatchPattern":"const MAX_UPLOAD_ATTEMPTS = 2;\nfor (let i = 0; i < MAX_UPLOAD_ATTEMPTS; i++) {\n  try { await postTweet({ text, images }); break; }\n  catch (e) {\n    if (e.name === 'TimeoutError' && e.message.includes('twitter image upload') && i < MAX_ATTEMPTS - 1) continue;\n    throw e;\n  }\n}","preventionTips":["Compress images below ~5MB before attaching","Stick to jpg/png and standard dimensions","Retry once on TimeoutError before escalating","Keep the browser session fast/logged in so X's media pipeline responds"],"tags":["timeout","image-upload","twitter"],"backgroundTag":"upload-timeout","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}