How to Handle Local Files in Puppeteer and Playwright
Upload files, respond to file chooser requests, and save browser downloads with practical Puppeteer and Playwright examples.
Short answer: In Puppeteer, upload through an input[type=file] with ElementHandle.uploadFile(), or wait for a file chooser before clicking and accept paths. In Playwright, prefer a locator’s setInputFiles(); for a dynamically created input, wait for the filechooser event and call setFiles(). Playwright exposes a Download object for downloads. Puppeteer’s current Files guide says it does not provide programmatic download handling. The examples below use JavaScript and Node.js.
1. Choose the right file workflow
There are three distinct jobs that are often conflated:
- Upload a file: set files on the page’s file input. You usually do not need to open or interact with the operating system’s file picker.
- Handle a chooser: wait for the page to request a file, then programmatically accept files or cancel the request.
- Save a download: observe a browser download and write it to a path. Playwright provides a Download API; Puppeteer’s current guide does not document a programmatic download API.
Both upload methods set the browser input’s selected files. The application still needs to process the selection, for example by submitting a form or handling its change event. Consult the Puppeteer Files guide, the Playwright file upload guide, and the Playwright Download API.
2. Puppeteer: upload through a file input
Use this when the page contains a file input, even if it is visually hidden behind a styled upload button. Find the actual input and call uploadFile() with one or more local paths.
const fileInput = await page.waitForSelector('input[type="file"]');
if (!fileInput) throw new Error('File input was not found');
await fileInput.uploadFile('/absolute/path/to/report.pdf');
To select multiple files, pass multiple paths:
await fileInput.uploadFile(
'/absolute/path/to/first.txt',
'/absolute/path/to/second.txt'
);
The input must allow multiple files for the page to treat the selection as a multi-file upload. Use a selector tied to the specific input if the page has more than one upload field. After setting the files, trigger the same page workflow as a user, such as clicking Submit, and wait for an application-specific success state.
3. Puppeteer: handle a file chooser
Use a chooser when clicking a button causes the page to request a file, particularly when the input is created dynamically. Start waiting before the action that opens it; the waiter does not return a chooser that is already open.
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.click('#upload-file-button'),
]);
await fileChooser.accept(['/absolute/path/to/report.pdf']);
For a chooser that accepts multiple files, pass multiple paths to accept(). You can inspect fileChooser.isMultiple() first. To close the chooser without selecting a file, call await fileChooser.cancel(). Every chooser must be accepted or cancelled before another can appear. See the official waitForFileChooser reference and FileChooser reference.
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.click('#optional-upload-button'),
]);
if (fileChooser.isMultiple()) {
await fileChooser.accept([
'/absolute/path/to/first.txt',
'/absolute/path/to/second.txt',
]);
} else {
await fileChooser.cancel();
}
Puppeteer’s chooser interception does not support dialogs opened through DOM APIs such as window.showOpenFilePicker(). For ordinary file inputs, direct input upload is usually the simpler route.
4. Playwright: upload with setInputFiles
Playwright recommends locator-based interactions. Locate the file input by its accessible label when possible, then set one path, an array of paths, or an in-memory file object. This example is suitable inside a Playwright script or test that already has a page.
import path from 'node:path';
const fileInput = page.getByLabel('Upload file');
await fileInput.setInputFiles(path.resolve('fixtures/report.pdf'));
// Continue with the page's normal submission flow.
await page.getByRole('button', { name: 'Submit' }).click();
path.resolve() makes the path explicit relative to the Node process’s current working directory. Alternatively, use a path anchored to the script directory. Multiple files, clearing a selection, and supplying bytes from memory work as follows:
import path from 'node:path';
// Multiple files. The input must support multiple selection.
await page.getByLabel('Upload files').setInputFiles([
path.resolve('fixtures/first.txt'),
path.resolve('fixtures/second.txt'),
]);
// Clear the selected files.
await page.getByLabel('Upload file').setInputFiles([]);
// Supply generated contents without creating a temporary file.
await page.getByLabel('Upload file').setInputFiles({
name: 'note.txt',
mimeType: 'text/plain',
buffer: Buffer.from('Generated file contents\n'),
});
The documented file object uses name, mimeType, and buffer. A directory path can also be supplied when the page exposes a directory upload input. See Playwright’s upload documentation for supported forms and path behavior.
5. Playwright: wait for a dynamic file chooser
When the file input is created only after a click, subscribe to the event first, trigger the action, and then set files on the resulting chooser.
import path from 'node:path';
const chooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Choose file' }).click();
const chooser = await chooserPromise;
await chooser.setFiles(path.resolve('fixtures/report.pdf'));
Registering the event wait before the click avoids missing a fast event. If a chooser action may fail before producing the event, give the wait a bounded timeout or ensure the page is in the expected state before starting the sequence.
6. Handle downloads in Playwright
Start waiting for the download before clicking the control that initiates it. Then use the returned Download object to save the file to a known path. The browser’s suggested filename is available from the download object.
import path from 'node:path';
const downloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: 'Download report' }).click();
const download = await downloadPromise;
const suggestedName = download.suggestedFilename();
const destination = path.resolve('downloads', suggestedName);
await download.saveAs(destination);
console.log(`Saved download to ${destination}`);
Make sure the destination directory exists before calling saveAs(). For example, create it with Node’s mkdir using { recursive: true }. Check whether the download failed before treating the saved file as valid; Playwright’s Download API documents failure(), saveAs(), path(), suggestedFilename(), and delete(). A download’s temporary path is managed by the browser context, so use saveAs() when the file needs to persist at a known location. See the Download API reference and Page download event reference.
7. What about Puppeteer downloads?
Puppeteer’s current Files guide says it does not offer programmatic download handling. If downloading is central to the workflow, Playwright’s documented Download object is the direct built-in path described here. Avoid relying on undocumented browser internals as though they were a stable equivalent API.
8. Paths, remote browsers, and generated files
A path is resolved in the environment that can access the file, and that environment may not be the one you are looking at in a terminal. The automation process needs read access to upload sources and write access to download destinations.
- Local run: resolve paths from a known working directory or from the script’s directory. Playwright documents relative paths as relative to the current working directory.
- Container or CI run: ensure fixtures are present in the job or container filesystem and that the process user can read them. Create output directories before saving.
- Remote Chrome with Puppeteer: Puppeteer specifically warns that a local script connected to remote Chrome must pass absolute paths to
FileChooser.accept(). Confirm the remote browser setup can access the file at that path. - Generated content: Playwright can accept an in-memory buffer object, avoiding temporary-file cleanup. The cited Puppeteer Files guide establishes path-based upload; it does not establish an equivalent in-memory upload pattern.
Do not assume that a path on your laptop exists on a remote browser host. Likewise, a download written inside a container is not automatically present on the host unless your environment exposes or copies that output.
9. Reliability and performance practices
- Wait for the right condition: wait for the input or chooser event before acting. After selection, assert the application’s visible confirmation or completed state instead of assuming that setting the file means the server accepted it.
- Use explicit paths: resolve paths once and log the resolved path when diagnosing failures. This makes working-directory mistakes easier to spot.
- Keep fixtures small and purposeful: large uploads increase transfer time and can make retries expensive. Use representative files for coverage, including a boundary-size file only when size handling is part of the test.
- Isolate output names: avoid parallel tests writing the same download filename. Use per-test directories or unique names, then clean them up after assertions.
- Bound waits and clean up: a missing chooser or download should fail with a useful timeout, not hang indefinitely. Cancel a Puppeteer chooser when the test intentionally abandons it.
- Validate the result: for downloads, check that the file exists and has expected content or metadata; for uploads, verify the application’s response, not merely the input’s selected state.
Neither official guide cited here gives universal throughput or file-size limits. Actual timing depends on the page, file, machine, network, and remote-browser topology. Measure the workflow in its target environment rather than applying an assumed benchmark.
10. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
waitForSelector times out |
The input is created later, is in another frame, or the selector does not match. | Confirm the selector and frame; for a click-triggered input, wait for the chooser event instead. |
| Chooser wait times out | The waiter started after the click, or the click did not trigger a native file input chooser. | Start the wait before the triggering action and verify the button’s behavior. Puppeteer does not intercept showOpenFilePicker(). |
| File path does not exist | Relative paths resolve from an unexpected working directory, or the browser process cannot see the local file. | Use a resolved absolute path, check file existence and permissions in the automation environment, and account for remote browser filesystem boundaries. |
| Puppeteer accepts a path but the upload fails | accept() does not validate that paths exist; the wrong path can therefore fail later. |
Check the path before accepting it and ensure the chooser permits the number of files supplied. |
| Only one file appears selected | The input or chooser does not permit multiple files. | Inspect the page’s input configuration or Puppeteer’s isMultiple(); test single-file behavior when multiple selection is unavailable. |
| Files are selected but no upload occurs | Selection and submission are separate steps, or the application ignores the change. | Follow the page’s submit flow and wait for its success state or response. |
| Playwright download wait times out | The click did not start a download, navigation or application state blocked it, or the event wait was registered too late. | Register waitForEvent('download') first and confirm the control initiates a browser download. |
| Download cannot be saved | The destination directory is missing or the process lacks write permission. | Create the directory first, use a writable absolute destination, and inspect the download failure result. |
| Later chooser never appears in Puppeteer | An earlier chooser was neither accepted nor cancelled. | Resolve every chooser with accept() or cancel() before opening another. |
11. Or skip the browser setup
If your goal is a screenshot of a web page rather than automating local file inputs or downloads, ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. It returns an image or PDF from one GET request. The API parameters used by other screenshot APIs also work, which can make switching easier. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Cookie and consent banners are accepted like a visitor and removed before capture, along with supported newsletter popups and chat widgets; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, no card required.
12. FAQ
Can I upload without displaying the operating system file picker?
Yes. Set the file input directly with Puppeteer’s uploadFile() or Playwright’s setInputFiles().
Can Playwright upload content that exists only in memory?
Yes. Pass an object with a filename, MIME type, and buffer to setInputFiles().
Can I use Puppeteer to detect downloads?
Puppeteer’s current Files guide says programmatic download handling is not offered. Playwright documents a Download object and download event.
Why does my upload input show the file but the form stay unchanged?
File selection supplies the input value; the page still has to process that change or submit the form. Assert the application’s result separately.


