File Transfers
File uploads and downloads require specific handling when the browser runs on a remote server instead of your local machine. This page covers both directions.
These methods work on any CDP connection (Puppeteer, or a Playwright CDP session), regardless of which region the browser runs in.
- A Browserless API token from your account dashboard
- Puppeteer or Playwright installed locally
Uploading files
Browserless.uploadFile attaches base64-encoded file data to an <input type="file"> on the page. Send the bytes from your machine and Browserless builds the File in the browser context and fires the input/change events a real selection would. The page's upload handlers run as if a user picked the file.
import puppeteer from "puppeteer-core";
import fs from "fs";
import path from "path";
const TOKEN = "YOUR_API_TOKEN_HERE";
const BROWSER_WS_ENDPOINT = `wss://production-sfo.browserless.io?token=${TOKEN}`;
const fileToUpload = {
name: "image.png",
content: fs
.readFileSync(path.join(process.cwd(), "image.png"))
.toString("base64"),
mimeType: "image/png",
};
const browser = await puppeteer.connect({
browserWSEndpoint: BROWSER_WS_ENDPOINT,
});
const page = await browser.newPage();
await page.goto("https://jimpl.com/");
await page.waitForSelector("input[type=file]");
const cdp = await page.createCDPSession();
const result = await cdp.send("Browserless.uploadFile", {
selector: "input[type=file]",
files: [fileToUpload],
});
console.log(result); // { ok: true }
await browser.close();
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
selector | string | — | CSS selector for the target <input type="file">. Must resolve to a file input or the call returns InvalidTarget |
files | object[] | — | One or more files to attach. Pass several to populate a multi-file input |
files[].content | string | — | Base64-encoded file contents |
files[].name | string | "file" | Filename reported to the page |
files[].mimeType | string | inferred from name | MIME type. When omitted, Browserless infers it from the filename extension |
The combined decoded size of all files in one call is capped at 50 MB. The bytes ride the CDP WebSocket as base64, so an unbounded payload would stall the connection. Over the limit returns { ok: false, error: "FileTooLarge" }.
Response
{ "ok": true }
On failure, ok is false and error carries the reason: SelectorNotFound (no element matched), InvalidTarget (the element isn't a file input), FileTooLarge, or a validation message for a missing selector/files.
Downloading files
Enable Browserless.setDownloadEnabled, then listen for Browserless.fileDownloaded. When a download finishes on the remote browser, Browserless reads it back off disk and streams it to you as base64. You never have to know the download directory or watch the filesystem.
Download events are off by default, since the file bytes count against your data transfer. You must opt in per session.
import puppeteer from "puppeteer-core";
import fs from "fs";
import path from "path";
const TOKEN = "YOUR_API_TOKEN_HERE";
const BROWSER_WS_ENDPOINT = `wss://production-sfo.browserless.io?token=${TOKEN}`;
const browser = await puppeteer.connect({
browserWSEndpoint: BROWSER_WS_ENDPOINT,
});
const page = await browser.newPage();
const cdp = await page.createCDPSession();
await cdp.send("Browserless.setDownloadEnabled", { enabled: true });
cdp.on("Browserless.fileDownloaded", ({ filename, data }) => {
const buffer = Buffer.from(data, "base64");
fs.writeFileSync(path.join(process.cwd(), filename), buffer);
console.log(`saved: ${filename} (${buffer.length} bytes)`);
});
await page.goto("https://scraping-sandbox.netlify.app/downloadsamples");
await page.click('a[href="/samples/sample.json"]');
// Give the download time to finish before closing the connection.
await new Promise((res) => setTimeout(res, 3000));
await browser.close();
Browserless.fileDownloaded event
| Field | Type | Description |
|---|---|---|
filename | string | The browser's suggested filename for the download |
mimeType | string | MIME type inferred from the filename extension |
size | number | Decoded file size in bytes |
data | string | Base64-encoded file contents |
The event fires once per completed download. Register the listener before the click that triggers the download so you don't miss it.
Playwright
Playwright's Download.saveAs() already handles remote browsers natively, so you don't need the CDP method. Register a download listener and save directly.
import playwright from "playwright-core";
const TOKEN = "YOUR_API_TOKEN_HERE";
const BROWSER_WS_ENDPOINT = `wss://production-sfo.browserless.io/chromium/playwright?token=${TOKEN}`;
const browser = await playwright.chromium.connect(BROWSER_WS_ENDPOINT);
const page = await browser.newPage();
await page.goto("https://scraping-sandbox.netlify.app/downloadsamples");
// Register the listener before clicking so the download can't be missed.
const downloadPromise = page.waitForEvent("download");
await page.click('a[href="/samples/sample.json"]');
const download = await downloadPromise;
// Saves on the machine running this script, not the remote browser.
await download.saveAs(download.suggestedFilename());
await browser.close();
FAQ & Troubleshooting
Why am I getting a 403 Forbidden error?
Your API token is missing or expired. Pass it as a ?token= query parameter in the WebSocket or HTTP URL. Verify the token in your account dashboard.
My script works locally but fails on Browserless
Local browser settings may differ from the Browserless environment. Use launch parameters to match your local setup (viewport, user agent, timezone). See launch parameters for the full list.