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.
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 — so 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 100 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 — so 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();
Next Steps
- Session Management - Manage browser sessions and persisted state
- Bot Detection - Bypass anti-bot protections with stealth and unblock modes
- Hybrid Automation - Combine automated scripts with manual browser interaction