jackwener/OpenCLI · error · CommandExecutionError

Image upload failed (base64 fallback): ${upload?.error ?? 'u

Error message

Image upload failed (base64 fallback): ${upload?.error ?? 'unknown error'}

What it means

attachImagesViaDataTransfer falls back to injecting image Files via a DataTransfer onto the composer's file input when direct paste/drop fails. If that fallback evaluate returns upload.ok === false (or nothing at all), this error is thrown embedding the browser-side failure reason (`upload.error`) or 'unknown error'. It means the image never reached the composer through the base64 fallback path.

Source

Thrown at clis/twitter/post.js:230

        try {
            Object.defineProperty(input, 'files', { value: dt.files, writable: false, configurable: true });
            assigned = input.files && input.files.length >= ${JSON.stringify(absPaths.length)};
        } catch(e) {
            try {
                const nativeInputFileSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'files');
                if (nativeInputFileSetter && nativeInputFileSetter.set) {
                    nativeInputFileSetter.set.call(input, dt.files);
                    assigned = input.files && input.files.length >= ${JSON.stringify(absPaths.length)};
                }
            } 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 };
    })()`), 'Twitter image upload fallback');
    if (!upload?.ok) {
        throw new CommandExecutionError(`Image upload failed (base64 fallback): ${upload?.error ?? 'unknown error'}`);
    }
}

async function submitTweet(page, text) {
    const clickResult = requirePostActionResult(await page.evaluate(`(async () => {
        try {
            const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
            for (const toast of Array.from(document.querySelectorAll('[role="alert"], [data-testid="toast"]'))) {
                if (visible(toast)) toast.setAttribute('data-opencli-before-submit-toast', 'true');
            }
            const buttons = Array.from(document.querySelectorAll('[data-testid="tweetButtonInline"], [data-testid="tweetButton"]'));
            const btn = buttons.find((el) => visible(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true');
            if (!btn) return { ok: false, message: 'Tweet button is disabled or not found.' };
            btn.click();
            return { ok: true };
        } catch (e) { return { ok: false, message: String(e) }; }
    })()`), 'Twitter post click');
    if (!clickResult?.ok) return clickResult;

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Read the `upload.error` text in the message — 'Could not assign files to input' means update the input selector in attachImagesViaDataTransfer to match the current X DOM
  2. Confirm the images are supported types (jpg/png/gif/webp) and within the 4-image limit before attaching (validateImagePaths already enforces paths, verify content too)
  3. Run on a real compose/post page with a loaded session so a file input exists
  4. Upgrade the browser/automation driver if DataTransfer File assignment is unsupported

Example fix

// before: single selector, fails silently when X changes DOM
const input = document.querySelector('input[data-testid="fileInput"]');
// after: search broadly and report why nothing was found
const input = document.querySelector('input[type="file"][accept*="image"]');
if (!input) return { ok: false, error: 'no file input present on compose page' };
Defensive patterns

Strategy: fallback

Validate before calling

const allowed = ['image/jpeg','image/png','image/gif','image/webp'];
for (const p of paths) { const t = mimeOf(p); if (!allowed.includes(t)) throw new Error(`unsupported image type: ${p}`); }
if (paths.length > 4) throw new Error('max 4 images per tweet');

Type guard

function canAttemptUpload(page) { return page != null && typeof page.evaluate === 'function'; }

Try / catch

try {
  await attachImagesViaDataTransfer(page, files);
} catch (e) {
  if (String(e.message).startsWith('Image upload failed (base64 fallback)')) { console.error('fallback upload failed:', e.message, '- update input selector or check image types'); }
  throw e;
}

Prevention

When it happens

Trigger: The in-page fallback script fails with 'Could not assign files to input' (no visible file input found, or DataTransfer constructor unavailable), files rejected by the input, or the change/input events dispatched but the composer didn't accept them — all surfaced here via requirePostActionResult(upload, 'Twitter image upload fallback').

Common situations: X removed/renamed the hidden input[type=file] the fallback targets; browser or headless environment lacking DataTransfer/File support (older drivers); images of unsupported type/size silently rejected; page not on the compose screen so no input exists to assign.

Related errors


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