# Drive a browser

URL: https://briq.cloud/docs/guides/drive-a-browser

> Open pages, fill forms, take screenshots and PDFs with a headless Chromium, from an agent that can run code or one that cannot.

Recipe:

- [Headless Chrome](https://briq.cloud/cookbook/chrome.md): A headless Chromium. Without a runtime of your own, drive it with briq_exec: a Puppeteer script posted to its /function API navigates, clicks, fills forms and makes PDFs and screenshots. With one, connect Playwright or Puppeteer to its WebSocket endpoint.

The [chrome recipe](https://briq.cloud/cookbook/chrome) runs a headless Chromium on its own briq. There are two ways
to drive it. Pick the one your agent can use:

| Your agent                                   | Drive it with                                 | One call costs                                  |
| -------------------------------------------- | --------------------------------------------- | ----------------------------------------------- |
| Has only briq's tools (ChatGPT, a chat app)  | `briq_exec` and the browser's `/function` API | A new browser: 1.5 to 2 s, plus the pages       |
| Runs code (Claude Code, Codex CLI, a server) | Playwright or Puppeteer, over a WebSocket     | 1 to 2 s to connect, then about 10 ms an action |

First, [connect your agent](https://briq.cloud/docs/getting-started), then start the browser:

```text
briq_run({ recipe: "chrome" })
→ chrome running · BROWSER_WS_ENDPOINT=ws://…:3000?token=… · BROWSERLESS_TOKEN=…
```

## With briq\_exec

`briq_exec` posts a Puppeteer script to the browser's `/function` API, inside the briq:

```text
briq_exec({
  briq: "bq_…",
  cmd: ["sh", "-c", "curl -s -X POST \"http://[::1]:3000/function?token=$TOKEN\" -H 'Content-Type: application/javascript' --data-binary @-"],
  stdin: "<the script below>"
})
```

```js
export default async function ({ page }) {
  await page.goto('https://httpbin.org/forms/post');
  await page.type('input[name=custname]', 'Ada');
  await page.click('input[value=medium]');
  await Promise.all([page.waitForNavigation(), page.click('form button')]);
  return { url: page.url(), text: await page.$eval('body', (b) => b.innerText) };
}
```

1. `curl` sends the script to the browser. `$TOKEN` is already in the briq's environment.
2. The briq launches a browser, runs the function with a Puppeteer `page`, then closes the browser.
3. The function's return value is the answer: a string as text, an object as JSON.

### Screenshots, PDFs and downloads

```sh
curl -s -X POST "http://[::1]:3000/screenshot?token=$TOKEN" -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com"}' -o /tmp/page.png
curl -s -X POST "http://[::1]:3000/pdf?token=$TOKEN" -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com"}' -o /tmp/page.pdf
```

Then `briq_files_get({ briq, path: "/tmp/page.png" })` shows you the screenshot. A PDF comes back
as base64, up to 1 MB. `/content` answers with the page's HTML. `/download` takes a function, like
`/function`, and answers with the file the page downloaded.

Inside a function, return binary as base64: `await page.screenshot({ encoding: 'base64' })`.

### Keep a session between calls

Each call gets a new browser, so cookies and open pages are gone. Do a task in one function when
you can: sign in, act, return. Across calls, return the cookies and send them back:

```js
// Call 1, saved with `curl … -o /tmp/cookies.json`
export default async function ({ page }) {
  await page.goto('https://example.com/login');
  // … sign in …
  const cdp = await page.createCDPSession();
  return (await cdp.send('Network.getCookies')).cookies;
}
```

```js
// Call 2, sent as JSON: { "code": "<this script>", "context": { "cookies": <the file> } }
export default async function ({ page, context }) {
  await page.setCookie(...context.cookies);
  await page.goto('https://example.com/account');
  return await page.title();
}
```

Build that JSON with `node`, which the image has:

```sh
node -e 'const f = require("fs"); process.stdout.write(JSON.stringify({ code: f.readFileSync("/tmp/next.js", "utf8"), context: { cookies: JSON.parse(f.readFileSync("/tmp/cookies.json", "utf8")) } }))' \
  | curl -s -X POST "http://[::1]:3000/function?token=$TOKEN" -H 'Content-Type: application/json' --data-binary @-
```

## With Playwright or Puppeteer

From another briq of the same stack, connect to `BROWSER_WS_ENDPOINT`:

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(process.env.BROWSER_WS_ENDPOINT);
const page = await browser.contexts()[0].newPage();
await page.goto('https://example.com');
```

* **Puppeteer**: `puppeteer.connect({ browserWSEndpoint: BROWSER_WS_ENDPOINT })`.
* **Playwright's own protocol**: `chromium.connect()` on the same address with
  `/chromium/playwright` as the path.
* **One script, one browser**: the browser closes when the script disconnects. To keep a sign-in
  for the next script, save it (`context.storageState()`) and load it again.

### From outside briq

Publish the browser with a private URL:

```text
briq_run({ recipe: "chrome", expose: "private" })
→ URL=https://chrome-ab12.briq.run/?briq_token=…
```

Send the URL's token as a cookie, and the browser's own token in the address:

```js
const browser = await chromium.connectOverCDP(
  `wss://chrome-ab12.briq.run/?token=${BROWSERLESS_TOKEN}`,
  { headers: { cookie: `briq_token=${BRIQ_TOKEN}` } },
);
```

Puppeteer takes the same `headers` in `connect`. Each action then also travels between your
machine and the briq's [region](https://briq.cloud/docs/regions).

## When it does not work

* **`Request has timed out`**: the call ran past 50 seconds. A selector never matched, a page never
  finished loading, or it was the first call on a new briq. Set a limit per call (`&timeout=20000`
  in ms) or split the task.
* **The first call on a new briq is slow**, up to about 30 seconds: Chromium's first launch.
* **`401` from a private URL**: the `briq_token` cookie is missing. A token in the URL's query
  string does not work for a WebSocket: the client follows the redirect without the cookie.
* **`400` when connecting**: an address with a query parameter the browser does not know. It takes
  `token`, `timeout`, `launch`, `blockAds`, `trackingId` and `profile`.
* **`page.cookies()` never returns** inside `/function`. Use `Network.getCookies`, as above.
* **Calls get slower and slower** on `small` or `medium`: steady work uses up a shared CPU's
  allowance. Use `size: "large"`, a dedicated CPU.
* **A site refuses the browser**: it is a plain Chromium, with no proxies and no stealth mode.

## Good to know

* **Four browsers at once**, ten more calls queued, then refused. Several at once on `small` are
  slower each.
* **A tool call has about a minute**: most MCP clients stop waiting after 60 seconds.
* **The script's `page` is Puppeteer's**, not Playwright's, in `/function`.
* **A WebSocket counts as one visit when it opens**: a session longer than the briq's
  [`sleep`](https://briq.cloud/docs/concepts/lifecycle-and-sleep) needs a longer one.
* **Logs show warnings and errors only**, never the token.
