Recommended Free Tools
Use Puppeteer’s ElementHandle.uploadFile() when the page contains an <input type="file">. If the site opens a native chooser after a button click, call page.waitForFileChooser() before the click and pass absolute paths to fileChooser.accept(). To automate an already-running Chrome instance, connect with puppeteer.connect({ browserWSEndpoint }). The browser endpoint attaches your script to Chrome; it does not transfer files, so the browser process must be able to read every path you provide.
What you need before uploading
- A Puppeteer project with a version you control. The current official documentation reviewed for this guide is from the Puppeteer 25.x documentation family; APIs can change, so pin and test the version used in production.
- A running Chromium or Chrome instance exposing a browser WebSocket endpoint.
- A file path readable by the environment in which Chrome runs. With a remote browser, a path on your laptop is not automatically present on the browser host.
- A page URL and either a file-input selector or a button that launches a chooser.
Keep the endpoint secret. A browser WebSocket URL can contain credentials, so load it from an environment variable rather than logging it or committing it to source control.
Connect to Chrome with browserWSEndpoint
puppeteer.connect() attaches to an externally launched browser. It does not start or stop that browser. The normal lifecycle is to connect, create or select a page, perform the upload, and call browser.disconnect() when your script should detach while leaving Chrome and its pages running. Use browser.close() only when this process owns the browser and is supposed to shut it down.
import puppeteer from 'puppeteer';
const browser = await puppeteer.connect({
browserWSEndpoint: process.env.BROWSER_WS_ENDPOINT,
});
try {
const page = await browser.newPage();
await page.goto('https://example.com/upload', {
waitUntil: 'networkidle2',
});
// Upload code goes here.
} finally {
browser.disconnect();
}
See Puppeteer’s browser-management guide and the ConnectOptions API for connection details. If the endpoint is unavailable, fix the browser launch, networking, or authentication problem before debugging file selection.
#1 Best Overall
Method 1: upload through a regular file input
The official Files guide says to locate a file input and call ElementHandle.uploadFile. This is the most reliable route because it avoids operating-system dialogs entirely.
Single-file upload
import puppeteer from 'puppeteer';
const browser = await puppeteer.connect({
browserWSEndpoint: process.env.BROWSER_WS_ENDPOINT,
});
try {
const page = await browser.newPage();
await page.goto('https://example.com/upload', { waitUntil: 'domcontentloaded' });
const input = await page.waitForSelector('input[type="file"]');
if (!input) throw new Error('File input was not found');
await input.uploadFile('/absolute/path/to/file.pdf');
// Submit if the application does not submit automatically.
await page.click('button[type="submit"]');
await page.waitForNetworkIdle();
} finally {
browser.disconnect();
}
waitForSelector() prevents a race with a dynamically rendered form. Replace the selector and path with values for your application. If several file inputs exist, use a narrower selector such as #avatar-input or a locator scoped to the correct form.
Multiple files
Pass multiple paths in one call when the input has the multiple attribute:
const input = await page.waitForSelector('input[type="file"][multiple]');
await input.uploadFile(
'/absolute/path/to/report.pdf',
'/absolute/path/to/data.csv',
);
Whether the application accepts several files is controlled by the page’s HTML and server-side validation. Puppeteer can set the selected files, but it cannot make a single-file input accept a list.
Confirm that the page received the files
Many applications render a filename, thumbnail, or validation message after the input changes. Wait for that application-specific signal before submitting:
await input.uploadFile('/absolute/path/to/photo.jpg');
await page.waitForSelector('.upload-preview, .filename');
await page.click('#save');
If the page submits through XHR or fetch, wait for a success element or a response that your application documents instead of relying only on a fixed delay.
Method 2: handle a button that opens a file chooser
When a custom upload button launches a chooser, install the waiter before triggering the action. Puppeteer documents that waitForFileChooser() “must be called before the file chooser is launched.” The waiter does not return a chooser that is already open.
Rank #2
const [chooser] = await Promise.all([
page.waitForFileChooser(),
page.click('#upload-file-button'),
]);
await chooser.accept(['/absolute/path/to/file.pdf']);
Starting both promises together is important: waiting after click() can miss a fast chooser event. The FileChooser API notes that browsers allow only one file chooser at a time. Complete or cancel one chooser before attempting another.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallChooser cancellation and replacement
Use chooser.cancel() when your flow must abort the selection. To replace a selection, call accept() again with the desired paths if the page and browser still hold the chooser state; otherwise trigger a new chooser and wait for it again.
const [chooser] = await Promise.all([
page.waitForFileChooser(),
page.click('#upload-file-button'),
]);
if (shouldUpload) {
await chooser.accept(['/absolute/path/to/file.pdf']);
} else {
await chooser.cancel();
}
Important chooser limitation
page.waitForFileChooser() intercepts the browser chooser event. It does not intercept DOM APIs such as window.showOpenFilePicker. If the site uses that API, use the application’s supported upload mechanism or an app-specific test hook rather than assuming the FileChooser API will fire.
Complete dynamic-upload example with browserWSEndpoint
This example supports both common page designs. It first looks for a conventional input; if none appears, it uses the chooser-triggering button. Adapt the selectors and success condition to the target application.
import puppeteer from 'puppeteer';
const endpoint = process.env.BROWSER_WS_ENDPOINT;
const filePath = process.env.UPLOAD_FILE;
if (!endpoint || !filePath) {
throw new Error('Set BROWSER_WS_ENDPOINT and UPLOAD_FILE');
}
const browser = await puppeteer.connect({ browserWSEndpoint: endpoint });
try {
const page = await browser.newPage();
await page.goto('https://example.com/upload', {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
const input = await page.$('input[type="file"]');
if (input) {
await input.uploadFile(filePath);
} else {
const button = await page.waitForSelector('#upload-file-button');
const [chooser] = await Promise.all([
page.waitForFileChooser(),
button.click(),
]);
await chooser.accept([filePath]);
}
await page.click('button[type="submit"]');
await page.waitForSelector('.upload-success', { timeout: 60_000 });
console.log('Upload completed');
} finally {
browser.disconnect();
}
The chooser and input branches are alternatives, not two steps to run together. Calling both against the same page can create conflicting events or a second upload.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Remote-browser path rules
The FileChooser.accept API explicitly warns that it “will not validate whether the file paths exists.” A successful accept() call therefore does not prove that Chrome could read the files.
When controller and browser share a machine
Use absolute paths such as /srv/uploads/file.pdf (Linux) or an absolute Windows path. Confirm the process user running Chrome has read permission, and avoid deleting or replacing the file until the upload has finished.
When Chrome is remote
Stage the file on the remote host first, using the provider’s documented transfer mechanism, shared volume, or object-storage download. Then pass the resulting absolute path visible to that host. A path like /Users/alex/Desktop/file.pdf from your laptop normally means nothing inside a remote container.
The Puppeteer documentation establishes the API and path requirement, but it does not define file staging or authentication for a particular remote-browser provider. Treat those as provider-specific responsibilities.
Waiting, validation, and reliability
Prefer state-based waits
- Wait for the input or upload button with
waitForSelector(). - After selecting a file, wait for the page’s preview, validation message, or enabled submit button.
- After submission, wait for a success element or a known response rather than an arbitrary sleep.
- Use a timeout appropriate to file size and server behavior; a slow upload can outlast the default navigation timeout.
Validate locally before invoking Puppeteer
Because accept() does not check existence, validate the path in Node.js and report a useful error before opening the chooser:
import fs from 'node:fs/promises';
await fs.access(filePath);
const stat = await fs.stat(filePath);
if (!stat.isFile()) throw new Error(`${filePath} is not a regular file`);
This check validates the controller’s filesystem. For a remote Chrome host, perform an equivalent check where the browser actually runs.
Keep browser ownership clear
Use browser.disconnect() after a remote session when the externally managed browser should continue serving other clients. Puppeteer documents that, unlike browser.close(), disconnecting does not shut down the browser or close its pages. Close only resources your process owns.
Troubleshooting common failures
“No file input found”
The form may render later, use a different selector, sit inside an iframe, or expose only a custom button. Wait for the page state, inspect the DOM, target the correct frame, or switch to the chooser pattern.
The chooser times out
Usually the waiter was installed after the click, the selector clicked a non-file control, or the site uses window.showOpenFilePicker. Put waitForFileChooser() and the triggering click in one Promise.all(), then verify the site’s upload implementation.
The call succeeds but no file arrives
Check that the path is absolute, exists in the browser environment, is readable by Chrome’s process user, and has the expected extension and MIME type. Remember that accept() does not validate paths.
Remote endpoint connection fails
Check the endpoint value, credentials, firewall rules, browser lifetime, and whether the endpoint is a WebSocket endpoint intended for Puppeteer. Do not print the full URL in logs if it contains a token.
The upload works manually but validation rejects it
Inspect client-side size/type limits and server responses. Puppeteer selects the file; the application still controls MIME checks, maximum size, required fields, CSRF handling, and authentication.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsPages disappear after the script ends
You probably called browser.close() on a browser managed elsewhere. Replace it with browser.disconnect() when the remote browser must remain alive.
Or skip the browser setup
If your goal is a clean image or PDF of a page rather than testing an upload workflow, ScreenshotNeo provides a website screenshot API and MCP server. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, with the response identifying the page verdict and billing status. Its MCP tools—take_screenshot, get_page_info, and capture_pdf—work with Claude, Cursor, and other MCP clients.
One GET request returns PNG, JPEG, WebP, or PDF output. See the ScreenshotNeo API documentation for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Every plan includes the features: full-page and element capture, device presets, custom viewport and retina scale, PDF controls, custom CSS and JavaScript, clicks and waits, request blocking, headers, cookies, user agent, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. Existing integrations can use the parameter names common to other screenshot APIs.
| Plan | Allowance and price |
|---|---|
| Free | 1,000 shots per month, no card |
| Starter | $5 for 3,000 shots |
| Growth | $15 for 15,000 shots |
| Pro | $39 for 60,000 shots |
| Scale | $99 for 250,000 shots |
| Business | $249 for 1,000,000 shots |
Yearly billing gives two months free, and every feature is on every plan. Create a free ScreenshotNeo account to get 1,000 screenshots each month without adding a card.
Best Value
FAQ
Can browserWSEndpoint upload a local file by itself?
No. It only connects Puppeteer to an existing browser. The file must be available to Chrome, and Puppeteer then sets the page input or chooser selection.
Do chooser paths have to be absolute?
For a local script controlling remote Chrome, use absolute paths and verify that the remote environment can read them.
Can waitForFileChooser handle every modern file picker?
No. It does not intercept window.showOpenFilePicker; that requires an application-specific strategy.
Free tools Windows power users keep installed
One-click scans. No signup required.
Should a connected browser be closed after each upload?
Only if your process owns it. For an externally managed browser, disconnect so Chrome and its pages remain available.
Frequently Asked Questions
Can browserWSEndpoint upload a local file by itself?
No. It connects Puppeteer to Chrome; the file must be readable by the browser environment.
Why did accept() return without an upload?
The API does not validate file existence. Check the absolute path and permissions where Chrome runs.
What if the page uses window.showOpenFilePicker?
waitForFileChooser does not intercept that DOM API; use an application-specific upload strategy.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →The Bottom Line
Use uploadFile() for a real file input and the pre-click waitForFileChooser() pattern for chooser buttons. With browserWSEndpoint, the decisive issue is file placement: Chrome—not merely your Node.js process—must be able to read the absolute path.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

