jackwener/OpenCLI · error · Error
Image upload failed: preview did not appear.
Error message
Image upload failed: preview did not appear.
What it means
Thrown by attachComposerImage when the file was technically assigned to the composer input but waitForComposerMediaReady(page, 1) never observes a preview thumbnail in the [data-testid="attachments"] area within MEDIA_UPLOAD_TIMEOUT_MS. This post-upload DOM check exists because a 200-style success from setFileInput can mask a silent no-attachment post.
Source
Thrown at clis/twitter/utils.js:198
if (nativeInputFileSetter && nativeInputFileSetter.set) {
nativeInputFileSetter.set.call(input, dt.files);
assigned = input.files && input.files.length > 0;
}
} catch(e2) { /* ignore */ }
}
if (!assigned) return { ok: false, error: 'Could not assign files to input' };
input.dispatchEvent(new Event('change', { bubbles: true }));
input.dispatchEvent(new Event('input', { bubbles: true }));
return { ok: true };
})()
`);
if (!upload?.ok) {
throw new Error(`Image upload failed: ${upload?.error ?? 'unknown error'}`);
}
}
const uploadState = await waitForComposerMediaReady(page, 1);
if (!uploadState?.ok) {
throw new Error(uploadState?.message ?? 'Image upload failed: preview did not appear.');
}
}
export function isRecoverableFileInputError(error) {
const msg = error instanceof Error ? error.message : String(error);
return /unknown action|not supported|not[-\s]?allowed|notallowederror/i.test(msg);
}
export async function waitForComposerMediaReady(page, expectedCount = 1) {
const iterations = Math.ceil(MEDIA_UPLOAD_TIMEOUT_MS / MEDIA_UPLOAD_POLL_MS);
return page.evaluate(`
(async () => {
const expected = ${JSON.stringify(expectedCount)};
for (let i = 0; i < ${JSON.stringify(iterations)}; i++) {
await new Promise(r => setTimeout(r, ${JSON.stringify(MEDIA_UPLOAD_POLL_MS)}));
const previewCount = document.querySelectorAll(
'[data-testid="attachments"] img, [data-testid="attachments"] video, [data-testid="attachments"] [role="group"], [data-testid="tweetPhoto"]'
).length;View on GitHub (pinned to 49907e53dc)
Solutions
- Re-run with a smaller, standard-format image (JPEG/PNG under a few MB) — X silently rejects files it cannot process
- Check network connectivity/latency; a slow upload can exceed MEDIA_UPLOAD_TIMEOUT_MS before the preview renders
- Reload the compose page and retry, confirming the preview thumbnail appears manually
- If the UI was redesigned, update the preview selectors used by waitForComposerMediaReady to the current [data-testid="attachments"] markup
Example fix
// before
await attachComposerImage(page, '/tmp/photo.heic'); // assigned, but X drops it -> preview never appears
// after
execSync('sips -s format jpeg /tmp/photo.heic --out /tmp/photo.jpg');
await attachComposerImage(page, '/tmp/photo.jpg'); Defensive patterns
Strategy: retry
Validate before calling
// Pre-encode check: only attach formats X reliably previews
const ok = /\.(png|jpe?g|webp|gif)$/i.test(absImagePath) && fs.statSync(absImagePath).size < 5 * 1024 * 1024;
if (!ok) throw new Error('Convert to a standard, reasonably sized JPEG/PNG/WebP/GIF before attaching'); Type guard
function composerMediaReady(state) {
return Boolean(state && typeof state === 'object' && state.ok === true);
} Try / catch
try {
await attachComposerImage(page, absPath);
} catch (err) {
if (err.message === 'Image upload failed: preview did not appear.') {
await new Promise(r => setTimeout(r, 2000));
await attachComposerImage(page, absPath); // single retry after slow upload
} else { throw err; }
} Prevention
- Attach standard formats (JPEG/PNG/WebP/GIF) at moderate sizes — X silently drops unsupported files
- Allow extra time on slow connections; the preview poll has a fixed timeout
- Reload the compose page between attempts so attachments start from a clean state
- Manually confirm the preview renders once after UI changes to detect selector drift
When it happens
Trigger: Files assignment succeeded but Twitter never started processing the upload: the composer's React handlers did not pick up the synthetic change/input events, the upload was rejected server-side (bad/corrupt file, unsupported format), the page navigated or the user closed the composer, or X changed its attachments DOM so the preview selectors no longer match.
Common situations: Attaching unsupported formats (e.g. huge GIFs or HEIC-renamed files) that X silently drops; slow networks where upload takes longer than the polling timeout; running on a modified/translated X UI where the preview selectors or remove-button labels no longer match; the image exceeding Twitter's own upload limits even though it passed the CLI's MAX_IMAGE_SIZE_BYTES check.
Related errors
- Image upload failed: ${msg}
- Image upload failed: ${upload?.error ?? 'unknown error'}
- Unexpected 12306 probe: ${JSON.stringify(probe)}
- ChatGPT did not create a conversation URL after sending the
- ChatWise response
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/d570c4ad7a309d5e.
Report an issue: GitHub.