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

  1. Locate the real <input type="file"> in the DOM (often hidden) and pass its selector.
  2. Make the hidden input selectable if needed, or resolve it via the wrapper: e.g. selector "form input[type=file]".
  3. For drag-only uploaders, dispatch drop events via page evaluate with a DataTransfer, or restructure the page test.
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/48a3bc9bfd66dbf4. Report an issue: GitHub.