Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
title: Browser Run adds session and DevTools methods to browser bindings
description: Browser Run browser bindings now support typed session management, outbound Worker routing, and DevTools methods.
products:
- browser-run
date: 2026-09-21
---

import { TypeScriptExample } from "~/components";

[Browser Run](/browser-run/) browser bindings now provide typed methods for session management and DevTools operations. You can acquire a session, connect a browser client, create Live View URLs, manage targets, and close sessions without constructing HTTP requests.

The new `acquire()` and `launch()` methods also accept [`outboundByHost`](/browser-run/features/outbound-workers/). This lets you route requests for selected hostnames through another Worker, including a Worker that adds authentication or reaches a private service.

<TypeScriptExample>

```ts
const connection = await env.BROWSER.launch({
outboundByHost: {
"private.example.test": env.OUTBOUND,
},
});
```

</TypeScriptExample>

Use `connectSession(sessionId)` when you need to acquire and connect in separate steps. The method returns a session-pinned `webSocket` Fetcher for a CDP client.

The binding also includes session methods for Live View, active sessions, session history, limits, session details, and cleanup. The nested `devtools` binding provides typed methods for browser version information, protocol descriptions, and target operations such as listing, creating, activating, and closing targets.

Refer to the [Browser binding API documentation](/browser-run/reference/browser-binding-api/) for method signatures and the [outbound Worker feature guide](/browser-run/features/outbound-workers/) for routing examples.
2 changes: 2 additions & 0 deletions src/content/docs/browser-run/cdp/session-management.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Use the HTTP API to manage browser sessions and tabs without using WebSocket con

The [API reference](/api/resources/browser_rendering/) documents all session management endpoints under `/devtools`.

When your code runs in a Worker, you can use the typed [Browser binding API](/browser-run/reference/browser-binding-api/) instead of constructing HTTP requests. The binding exposes `acquire()`, `connectSession()`, `launch()`, and a nested `devtools` target for session and target management.

## Step 1: Acquire a browser session

Create a new browser session using the `POST /devtools/browser` endpoint. The session will remain active for the specified keep-alive time (in this example, 10 minutes).
Expand Down
156 changes: 156 additions & 0 deletions src/content/docs/browser-run/features/outbound-workers.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
---
pcx_content_type: how-to
title: Outbound Workers
description: Route selected Browser Run requests through another Worker for private services, authentication, or response processing.
sidebar:
order: 8
products:
- browser-run
---

import { TypeScriptExample, WranglerConfig } from "~/components";

Use `outboundByHost` to send browser requests for selected hostnames through another Worker. The outbound Worker can reach a private service, add authentication, or transform the response before it reaches the browser.

This feature is useful when the browser should request a hostname that has no public DNS record. Browser Run matches the hostname and sends the request to the Worker Fetcher instead of requiring public DNS resolution.

## Configure the bindings

Declare a Browser Run binding and a service binding for the outbound Worker:

<WranglerConfig>

```toml
name = "browser-runner"
main = "src/index.ts"
compatibility_date = "$today"
compatibility_flags = ["nodejs_compat"]

[browser]
binding = "BROWSER"

[[services]]
binding = "OUTBOUND"
service = "outbound-worker"
```

</WranglerConfig>

The service binding gives the browser Worker a `Fetcher` for the outbound Worker. The outbound Worker does not need a public route.

## Route requests by hostname

Pass the service binding to `launch()` or `acquire()` in the `outboundByHost` map. The map key must exactly match the hostname in the browser request.

The following example uses raw CDP commands. It opens a Browser Run session, routes `private.example.test` through the outbound Worker, and sends a `Page.navigate` command:

<TypeScriptExample filename="src/index.ts">

```ts
interface Env {
BROWSER: Fetcher;
OUTBOUND: Fetcher;
}

type CdpResponse = {
id: number;
result?: unknown;
error?: { message: string };
};

let nextCdpCommandId = 0;

function sendCdpCommand(
socket: WebSocket,
method: string,
params: Record<string, unknown> = {},
): Promise<unknown> {
return new Promise((resolve, reject) => {
const id = ++nextCdpCommandId;
const timeout = setTimeout(() => {
cleanup();
reject(new Error(`CDP command "${method}" timed out`));
}, 30_000);

const cleanup = () => {
clearTimeout(timeout);
socket.removeEventListener("message", onMessage);
socket.removeEventListener("close", onClose);
};

const onClose = () => {
cleanup();
reject(new Error("CDP connection closed before the command completed"));
};

const onMessage = (event: MessageEvent<string>) => {
const message = JSON.parse(event.data) as CdpResponse;
if (message.id !== id) return;

cleanup();
if (message.error) {
reject(new Error(message.error.message));
} else {
resolve(message.result);
}
};

socket.addEventListener("message", onMessage);
socket.addEventListener("close", onClose);
try {
socket.send(JSON.stringify({ id, method, params }));
} catch (error) {
cleanup();
reject(error);
}
});
}

export default {
async fetch(_request: Request, env: Env): Promise<Response> {
const connection = await env.BROWSER.launch({
outboundByHost: {
"private.example.test": env.OUTBOUND,
},
});
const response = await connection.webSocket.fetch(
"https://browser-binding.invalid",
{ headers: { Upgrade: "websocket" } },
);
if (!response.webSocket) {
throw new Error("Browser Run did not return a WebSocket");
}

const socket = response.webSocket;
socket.accept();

try {
await sendCdpCommand(socket, "Page.navigate", {
url: "http://private.example.test",
});
return new Response("Navigation sent through the outbound Worker");
} finally {
socket.close();
await env.BROWSER.closeSession(connection.sessionId);
}
},
};
```

</TypeScriptExample>

The `.test` top-level domain is reserved for testing. Use a hostname that does not resolve on public DNS for a virtual outbound route. The hostname must still match the key in `outboundByHost`.

Outbound Worker routing supports HTTP requests only. Use an `http://` URL for the routed hostname. HTTPS requests do not use the outbound Worker.

## Constraints

- Use `outboundByHost` with Browser Run binding methods only. It is not supported by REST endpoints or legacy HTTP-only bindings.
- Create the Fetcher and call `acquire()` or `launch()` in the same Worker invocation.
- The Fetcher is not persisted with the browser session and cannot be reused by a later invocation.
- Outbound routing applies to the hostnames in the map. Requests to other hostnames use the browser's normal network path.

## Next steps

- Review the [Browser binding API](/browser-run/reference/browser-binding-api/).
- Learn how to use the [Chrome DevTools Protocol](/browser-run/cdp/).
Loading
Loading