For AI agents: a documentation index is available at /llms.txt
Skip to main content

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

ParameterTypeDefaultDescription
selectorstringCSS selector for the target <input type="file">. Must resolve to a file input or the call returns InvalidTarget
filesobject[]One or more files to attach. Pass several to populate a multi-file input
files[].contentstringBase64-encoded file contents
files[].namestring"file"Filename reported to the page
files[].mimeTypestringinferred from nameMIME 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

FieldTypeDescription
filenamestringThe browser's suggested filename for the download
mimeTypestringMIME type inferred from the filename extension
sizenumberDecoded file size in bytes
datastringBase64-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