jackwener/OpenCLI · error · TimeoutError

twitter image upload

Error message

twitter image upload

What it means

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.

Source

Thrown at clis/twitter/post.js:335

        // Attach media before inserting text. Uploading media after Draft.js has
        // text can re-render/reset the editor, causing image-only posts.
        if (absPaths.length > 0) {
            await page.wait({ selector: FILE_INPUT_SELECTOR, timeout: 20 });
            if (page.setFileInput) {
                try {
                    await page.setFileInput(absPaths, FILE_INPUT_SELECTOR);
                } catch (err) {
                    if (!isRecoverableFileInputError(err)) {
                        throw err;
                    }
                    await attachImagesViaDataTransfer(page, absPaths);
                }
            } else {
                await attachImagesViaDataTransfer(page, absPaths);
            }
            const uploadState = await waitForImageUpload(page, absPaths.length);
            if (!uploadState?.ok) {
                throw new TimeoutError('twitter image upload', UPLOAD_TIMEOUT_MS / 1000, 'Nothing was posted. Retry, or attach a smaller image.');
            }
        }

        // Insert and verify the text after media upload so text + images are in
        // the final Draft.js composer state immediately before clicking Post.
        const typeResult = await insertComposerText(page, text);
        if (!typeResult?.ok) {
            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.');
        }

        await page.wait(1);
        const result = await submitTweet(page, text);
        if (result?.unconfirmed) {
            // The poll expiring does not mean the tweet stayed in the composer,
            // so this must not read as a definite failure: the agent workflow
            // retries CommandExecutionError and would post twice (#2255).
            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.`);
        }

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Retry the command; transient upload slowness is the most common cause
  2. Attach smaller or fewer images (compress to under a few MB)
  3. Verify the image formats are jpg/png/gif/webp and readable by the browser session
  4. Check the browser session is healthy and logged in; re-run `opencli login` if the composer looked broken
  5. Pin/report if X changed the composer DOM so waitForImageUpload's selectors no longer match

Example fix

// before
await opencli('twitter post', { text: 'hi', images: 'huge-20mb.png' });
// after
const small = await compress('huge-20mb.png', { maxWidth: 1600 });
await opencli('twitter post', { text: 'hi', images: small });
Defensive patterns

Strategy: retry

Validate before calling

const files = images.split(',').map(s => s.trim());
const stat = await fs.stat(f);
if (!/\.(jpe?g|png|gif|webp)$/i.test(f)) throw new Error('unsupported format: ' + f);
if (stat.size > 5 * 1024 * 1024) throw new Error('image too large: ' + f);

Try / catch

const MAX_UPLOAD_ATTEMPTS = 2;
for (let i = 0; i < MAX_UPLOAD_ATTEMPTS; i++) {
  try { await postTweet({ text, images }); break; }
  catch (e) {
    if (e.name === 'TimeoutError' && e.message.includes('twitter image upload') && i < MAX_ATTEMPTS - 1) continue;
    throw e;
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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