cdpfleet Docs GitHub Dashboard

Cases / #12 · 2026-10-01 · Medium

Files across the wire: uploading to and downloading from a remote browser

The browser runs on our servers, your files live on your machine. setInputFiles and download.saveAs move them both ways — byte for byte.

Chromium

Run on production on 2026-10-01: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

The problem

Automations often upload a document (a CSV import, a profile photo) or download one (an invoice, an export). With a remote browser, the upload has to come from your disk and the download has to end up on your disk — not on the server running the browser. Does that work transparently, and are the files intact?

What we used, and why

WhatWhy
page.setInputFiles(selector, path)Reads the file on your machine and streams it to the remote browser's file input.
page.waitForEvent('download')Catches the download the click starts; the file is first saved on the browser's server.
download.saveAs(path)Streams the downloaded file from the server to your machine.
page.route on httpbin.orgServes a tiny page with an upload form and a download link on a real origin; httpbin echoes uploads back.
/bytes/102400?seed=42httpbin's seeded random bytes: the same seed always gives the same 100 KB, so the download can be checked against a direct copy.

How it works

  1. Write a ~60 KB CSV locally and hash it.
  2. Set it into the page's file input and submit; compare the hash of what the server received.
  3. Click the download link, save the file locally and compare its hash with the same bytes fetched directly.

The code

The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:

// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium } from 'playwright';
import { createHash, randomBytes } from 'node:crypto';
import { writeFileSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';

const KEY = process.env.CDPFLEET_API_KEY;
const sha256 = (buf) => createHash('sha256').update(buf).digest('hex');

// A local file to upload: 2,000 CSV rows (~60 KB).
const uploadPath = path.join(tmpdir(), 'cdpfleet-upload.csv');
const rows = ['id,token'];
for (let i = 1; i <= 2000; i++) rows.push(`${i},${randomBytes(12).toString('hex')}`);
writeFileSync(uploadPath, `${rows.join('\n')}\n`);
const local = readFileSync(uploadPath);

const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
  method: 'POST',
  headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
  body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: true }),
});
if (!res.ok) throw new Error(`launch: ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });

// A small page on httpbin.org's origin with an upload form and a download link.
const PAGE = `<form method="post" action="/anything" enctype="multipart/form-data">
  <input type="file" name="upload" id="file"><button id="send">Send</button></form>
<a id="data" href="/bytes/102400?seed=42" download="data.bin">data</a>`;

try {
  const page = await browser.newPage();
  await page.route('https://httpbin.org/files-demo', (route) => route.fulfill({ contentType: 'text/html', body: PAGE }));
  await page.goto('https://httpbin.org/files-demo', { timeout: 60000 });

  // Upload: setInputFiles reads the file HERE and streams it to the remote browser.
  let t = Date.now();
  await page.setInputFiles('#file', uploadPath);
  const [answer] = await Promise.all([page.waitForNavigation({ timeout: 60000 }), page.click('#send')]);
  const echoed = (await answer.json()).files.upload;
  const uploadMs = Date.now() - t;

  // Download: the file lands on the remote server; saveAs streams it back to this machine.
  await page.goto('https://httpbin.org/files-demo', { timeout: 60000 });
  t = Date.now();
  const [download] = await Promise.all([page.waitForEvent('download', { timeout: 60000 }), page.click('#data')]);
  const downloadPath = path.join(tmpdir(), download.suggestedFilename());
  await download.saveAs(downloadPath);
  const downloadMs = Date.now() - t;
  const got = readFileSync(downloadPath);
  // The same seeded bytes fetched directly from here, to prove the copy is exact.
  const direct = Buffer.from(await (await fetch('https://httpbin.org/bytes/102400?seed=42')).arrayBuffer());

  console.log(JSON.stringify({
    upload: {
      local_file: path.basename(uploadPath), bytes: local.length, sha256: sha256(local),
      server_received_bytes: Buffer.byteLength(echoed), server_sha256: sha256(Buffer.from(echoed)),
      identical: sha256(local) === sha256(Buffer.from(echoed)), ms: uploadMs,
    },
    download: {
      suggested_filename: download.suggestedFilename(), bytes: got.length, sha256: sha256(got),
      direct_sha256: sha256(direct), identical: sha256(got) === sha256(direct), ms: downloadMs,
    },
  }, null, 2));
} finally {
  await browser.close();
}

What we got

DirectionBytesIdenticalTime (ms)
upload cdpfleet-upload.csv58902yes3134
download data.bin102400yes1286

From the Node.js run on 2026-10-01. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)
{
  "upload": {
    "local_file": "cdpfleet-upload.csv",
    "bytes": 58902,
    "sha256": "f86a1140019393e500309dc68217d77ea6410128cc3dc8630629f423dacbaadd",
    "server_received_bytes": 58902,
    "server_sha256": "f86a1140019393e500309dc68217d77ea6410128cc3dc8630629f423dacbaadd",
    "identical": true,
    "ms": 3134
  },
  "download": {
    "suggested_filename": "data.bin",
    "bytes": 102400,
    "sha256": "3281a765f460e525539f06ff6f9811b0c5d3a081d30d50a50d90a10d6cfa78d1",
    "direct_sha256": "3281a765f460e525539f06ff6f9811b0c5d3a081d30d50a50d90a10d6cfa78d1",
    "identical": true,
    "ms": 1286
  }
}

Takeaways