can1357/oh-my-pi · error · ToolError
tab.uploadFile() requires an <input type="file"> element (go
Error message
tab.uploadFile() requires an <input type="file"> element (got <${tagName.toLowerCase()}>) What it means
Element.uploadFile() only works on <input type="file"> elements. The worker checks tagName inside the page and throws this ToolError naming the actual tag when the resolved element is not an INPUT. Note the check is tagName-only; a non-file input (e.g. type="text") will pass this gate but fail later inside uploadFile.
Source
Thrown at packages/coding-agent/src/tools/browser/tab-worker.ts:2077
}
async #uploadFile(
selector: string,
filePaths: string[],
timeoutMs: number,
signal: AbortSignal,
session: SessionSnapshot,
): Promise<void> {
if (!filePaths.length) throw new ToolError("tab.uploadFile() requires at least one file path");
const handle = await this.#resolveActionHandle(selector, timeoutMs, signal);
try {
const absolute = filePaths.map(filePath => resolveToCwd(filePath, session.cwd));
const upload = handle as unknown as { uploadFile: (...paths: string[]) => Promise<void> };
const tagName = (await untilAborted(signal, () =>
handle.evaluate(el => (el as unknown as { tagName: string }).tagName),
)) as string;
if (tagName !== "INPUT")
throw new ToolError(
`tab.uploadFile() requires an <input type="file"> element (got <${tagName.toLowerCase()}>)`,
);
await untilAborted(signal, () => upload.uploadFile(...absolute));
} finally {
await handle.dispose().catch(() => undefined);
}
}
async #waitForUrl(pattern: string | RegExp, timeout: number, signal: AbortSignal): Promise<string> {
const page = this.#requirePage();
const isRegex = pattern instanceof RegExp;
const matcher = isRegex ? pattern.source : pattern;
const flags = isRegex ? pattern.flags : "";
await untilAborted(signal, () =>
page.waitForFunction(
(m: string, isRe: boolean, fl: string) => {
const url = (globalThis as unknown as { location: { href: string } }).location.href;
return isRe ? new RegExp(m, fl).test(url) : url.includes(m);View on GitHub (pinned to 9690622007)
Solutions
- Locate the real <input type="file"> in the DOM (often hidden) and pass its selector.
- Make the hidden input selectable if needed, or resolve it via the wrapper: e.g. selector "form input[type=file]".
- For drag-only uploaders, dispatch drop events via page evaluate with a DataTransfer, or restructure the page test.
- Check the tag with a quick evaluate before calling if unsure.
Example fix
// before
await tab.uploadFile("#upload-button", ["./doc.pdf"]); // it's a <button>
// after
await tab.uploadFile("form input[type=file]", ["./doc.pdf"]); // hidden input behind the button Defensive patterns
Strategy: validation
Validate before calling
const info = await tab.evaluate((sel) => {
const el = document.querySelector(sel);
return { tag: el?.tagName, type: (el as HTMLInputElement)?.type ?? null };
}, selector);
if (info.tag !== "INPUT" || info.type !== "file") {
throw new Error(`${selector} is <${info.tag} type=${info.type}>; need <input type=file>`);
} Type guard
function isFileInput(info: { tag?: string | null; type?: string | null }): boolean {
return info.tag === "INPUT" && info.type === "file";
} Try / catch
try {
await tab.uploadFile(selector, filePaths);
} catch (err) {
if (err.message.includes("requires an <input type=\"file\">")) {
return tab.uploadFile("input[type=file]", filePaths); // locate the real input
}
throw err;
} Prevention
- Target the underlying <input type=file>, not the styled button/dropzone
- Search the DOM for input[type=file] (often hidden) before uploading
- For drag-only uploaders, dispatch drop events instead
- Check tagName/type via evaluate when unsure
When it happens
Trigger: tab.uploadFile("#dropzone", [...]) where #dropzone is a div drag-and-drop zone, a button that opens a native file picker, or any non-input element; also targeting the form rather than its file input.
Common situations: Custom upload widgets (styled divs/buttons wrapping a hidden <input type=file>); targeting the visible drop area instead of the underlying input; sites that use drag-drop only with no file input at all.
Related errors
- tab.select() requires a <select> element
- tab.ariaSnapshot: selector ${__sel} matched no element
- Element handle selector no longer resolves
- No element matched ${spec.raw}
- tab.uploadFile() requires at least one file path
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/48a3bc9bfd66dbf4.
Report an issue: GitHub.