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.

Uploading Files

Puppeteer's Page.uploadFile() reads from the server filesystem, not the client. To upload client-side data, create a virtual file in the browser context using DataTransfer and page.evaluate().

import puppeteer from "puppeteer-core";
import path from "path";
import fs from "fs";

const sleep = (ms) => new Promise((res) => setTimeout(res, ms));

const API_TOKEN = "YOUR_API_TOKEN_HERE";
const BROWSER_WS_ENDPOINT = `wss://production-sfo.browserless.io?token=${API_TOKEN}`;

const fileToUpload = {
name: "image.png",
content: fs
.readFileSync(path.join(process.cwd(), "image.png"))
.toString("base64"),
mimeType: "image/png",
};

const uploadFile = async (page, fileToUpload) => {
await page.evaluate(
({ selector, fileName, mimeType, base64Data }) => {
function b64ToUint8Array(b64) {
const binary = atob(b64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}

const input = document.querySelector(selector);
const file = new File([b64ToUint8Array(base64Data)], fileName, {
type: mimeType,
});

const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
input.files = dataTransfer.files;

const event = new Event("change", { bubbles: true });
input.dispatchEvent(event);
},
{
selector: "input[type=file]",
fileName: fileToUpload.name,
mimeType: fileToUpload.mimeType,
base64Data: fileToUpload.content,
}
);
};

(async () => {
const browser = await puppeteer.connect({
browserWSEndpoint: BROWSER_WS_ENDPOINT,
});
const page = await browser.newPage();

console.log("Navigating to jimpl.com...");
await page.goto("https://jimpl.com/");

console.log("Waiting for file input...");
await page.waitForSelector("input[type=file]");

await uploadFile(page, fileToUpload);

console.log("Waiting 5 seconds to allow upload...");
await sleep(5000);

await page.screenshot({ path: "imagecompressor_upload_result.png" });
console.log("Screenshot taken: imagecompressor_upload_result.png");

await browser.close();
})().catch((e) => {
console.error("Error:", e);
});
tip

Always trigger a change event after setting files on the input element, and set the correct mimeType for your file data.

Downloading Files

Downloads behave differently in local and remote browser environments. Playwright handles this natively. Puppeteer requires CDP network interception to capture file data before it hits the remote disk.

Playwright

Playwright's Download.saveAs() works out of the box with remote browsers.

import playwright from "playwright-core";

const API_TOKEN = "YOUR_API_TOKEN_HERE";
const BROWSER_WS_ENDPOINT = `wss://production-sfo.browserless.io/chromium/playwright?token=${API_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();

Puppeteer

Puppeteer uses the CDP Fetch domain and IO.read to stream file data from the remote browser, detecting downloads via the Content-Disposition: attachment header.

import puppeteer from "puppeteer-core";
import fs from "fs";

const API_TOKEN = "YOUR_API_TOKEN_HERE";
const BROWSER_WS_ENDPOINT = `wss://production-sfo.browserless.io/chromium/stealth?token=${API_TOKEN}`;

const browser = await puppeteer.connect({
browserWSEndpoint: BROWSER_WS_ENDPOINT,
});
const page = await browser.newPage();
const cdp = await page.createCDPSession();

// Pause responses so we can detect downloads and stream them back locally.
await cdp.send("Fetch.enable", {
patterns: [{ urlPattern: "*", requestStage: "Response" }],
});

const saved = new Promise((resolve) => {
cdp.on("Fetch.requestPaused", async (event) => {
const contentDisposition =
event.responseHeaders?.find((h) => h.name.toLowerCase() === "content-disposition")?.value ?? "";

// A download is a response the server marks as an attachment.
if (!contentDisposition.includes("attachment")) {
await cdp.send("Fetch.continueRequest", { requestId: event.requestId });
return;
}

const { stream } = await cdp.send("Fetch.takeResponseBodyAsStream", {
requestId: event.requestId,
});
const filename = event.request.url.split("/").pop();
const out = fs.createWriteStream(filename);

// IO.read streams the file over the websocket to the local filesystem.
for (;;) {
const chunk = await cdp.send("IO.read", { handle: stream, size: 256 * 1024 });
out.write(Buffer.from(chunk.data, chunk.base64Encoded ? "base64" : "utf8"));
if (chunk.eof) break;
}
out.end();

// Abort so the remote browser doesn't also download the file.
await cdp.send("Fetch.failRequest", { requestId: event.requestId, errorReason: "Aborted" });
resolve(filename);
});
});

await page.goto("https://scraping-sandbox.netlify.app/downloadsamples");
await page.click('a[href="/samples/sample.json"]');
console.log("saved:", await saved);
await browser.close();

Next Steps