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

  1. Re-run with a smaller, standard-format image (JPEG/PNG under a few MB) — X silently rejects files it cannot process
  2. Check network connectivity/latency; a slow upload can exceed MEDIA_UPLOAD_TIMEOUT_MS before the preview renders
  3. Reload the compose page and retry, confirming the preview thumbnail appears manually
  4. 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

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


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