diff --git a/src/content/changelog/browser-run/2026-09-21-browser-binding-methods.mdx b/src/content/changelog/browser-run/2026-09-21-browser-binding-methods.mdx new file mode 100644 index 00000000000..fa0c2ff3bd8 --- /dev/null +++ b/src/content/changelog/browser-run/2026-09-21-browser-binding-methods.mdx @@ -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. + + + +```ts +const connection = await env.BROWSER.launch({ + outboundByHost: { + "private.example.test": env.OUTBOUND, + }, +}); +``` + + + +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. diff --git a/src/content/docs/browser-run/cdp/session-management.mdx b/src/content/docs/browser-run/cdp/session-management.mdx index bc11a3b70fa..82e31d51f67 100644 --- a/src/content/docs/browser-run/cdp/session-management.mdx +++ b/src/content/docs/browser-run/cdp/session-management.mdx @@ -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). diff --git a/src/content/docs/browser-run/features/outbound-workers.mdx b/src/content/docs/browser-run/features/outbound-workers.mdx new file mode 100644 index 00000000000..418e322aa0e --- /dev/null +++ b/src/content/docs/browser-run/features/outbound-workers.mdx @@ -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: + + + +```toml +name = "browser-runner" +main = "src/index.ts" +compatibility_date = "$today" +compatibility_flags = ["nodejs_compat"] + +[browser] +binding = "BROWSER" + +[[services]] +binding = "OUTBOUND" +service = "outbound-worker" +``` + + + +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: + + + +```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 = {}, +): Promise { + 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) => { + 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 { + 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); + } + }, +}; +``` + + + +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/). diff --git a/src/content/docs/browser-run/reference/browser-binding-api.mdx b/src/content/docs/browser-run/reference/browser-binding-api.mdx new file mode 100644 index 00000000000..5e45bd4a03a --- /dev/null +++ b/src/content/docs/browser-run/reference/browser-binding-api.mdx @@ -0,0 +1,243 @@ +--- +pcx_content_type: reference +title: Browser binding API +description: Acquire, connect to, inspect, and manage Browser Run sessions from a Cloudflare Worker with typed browser binding methods. +sidebar: + order: 22 +products: + - browser-run +--- + +import { TypeScriptExample, WranglerConfig } from "~/components"; + +The Browser Run binding provides typed methods for browser session management from a [Cloudflare Worker](/workers/). Use these methods to acquire a session, connect a browser client, route browser requests through another Worker, and manage DevTools targets. + +Configure a [browser binding](/browser-run/reference/wrangler/#bindings) in your Wrangler configuration: + + + +```toml +name = "browser-binding-example" +main = "src/index.ts" +compatibility_date = "$today" +compatibility_flags = ["nodejs_compat"] + +[browser] +binding = "BROWSER" +``` + + + +## Session methods + +| Method | Description | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `acquire(options?)` | Creates a browser session and returns its session ID. Set `targets: true` to include the session's current targets. | +| `connectSession(sessionId, options?)` | Returns a connection object with a session-pinned `webSocket` Fetcher for connecting a CDP client to an existing session. | +| `launch(options?)` | Acquires a session and returns the same connection result as `connectSession()`. | +| `getLiveView(sessionId, options?)` | Creates a Live View URL for a session or target. | +| `listSessions(options?)` | Lists active sessions in the account. | +| `history(options?)` | Lists recent active and closed sessions. | +| `limits()` | Returns the current session limits and usage values. | +| `getSession(sessionId)` | Returns session details or `null` when the session does not exist. | +| `closeSession(sessionId)` | Closes a session and returns `closing` or `closed`. | +| `devtools` | Provides typed methods for the DevTools JSON endpoints. | + +## Acquire and connect to a session + +`acquire()` returns session metadata. Use `connectSession()` to get a session-pinned Fetcher, then open a WebSocket upgrade and send Chrome DevTools Protocol (CDP) commands. + + + +```ts +interface Env { + BROWSER: Fetcher; +} + +type CdpResponse = { + id: number; + result?: unknown; + error?: { message: string }; +}; + +let nextCdpCommandId = 0; + +function sendCdpCommand( + socket: WebSocket, + method: string, + params: Record = {}, +): Promise { + 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) => { + 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 { + const session = await env.BROWSER.acquire({ targets: true }); + const connection = await env.BROWSER.connectSession(session.sessionId); + 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: "https://example.com", + }); + const version = await sendCdpCommand(socket, "Browser.getVersion"); + return Response.json(version); + } finally { + socket.close(); + await env.BROWSER.closeSession(session.sessionId); + } + }, +}; +``` + + + +Use `launch()` when you do not need to separate acquisition from connection: + + + +```ts +interface Env { + BROWSER: Fetcher; +} + +async function useLaunch(env: Env): Promise { + const session = await env.BROWSER.launch(); + const response = await session.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 { + return await sendCdpCommand(socket, "Browser.getVersion"); + } finally { + socket.close(); + await env.BROWSER.closeSession(session.sessionId); + } +} +``` + + + +`launch()` performs the acquire and connection-capability steps in one call. Both methods return a connection object with this shape: + +| Property | Description | +| ----------- | --------------------------------------------------------- | +| `sessionId` | The Browser Run session ID. | +| `webSocket` | A session-pinned `Fetcher` used to open a CDP connection. | +| `targets` | The target list when requested with `targets: true`. | + +### Session options + +`acquire()` and `launch()` accept these options: + +| Option | Type | Description | +| ------------------------ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `keepAlive` | `number` | Session inactivity timeout in milliseconds. The value must be between 10 seconds and 20 minutes. | +| `recording` | `boolean` | Records the session for later inspection. | +| `location` | `string` | ISO 3166-1 alpha-2 country code for the browser location. | +| `outboundByHost` | `Record` | Routes requests for each hostname through a Worker Fetcher. Refer to [Route requests through an outbound Worker](/browser-run/features/outbound-workers/). | +| `guardrails` | `object` | Restricts the hostnames that the browser session can access. Refer to [Guardrails](/browser-run/features/guardrails/). | +| `targets` | `boolean` | Includes the session's DevTools targets in the result. | +| `liveViewUrlExpiresInMs` | `number` | Sets the expiry for target Live View URLs when `targets` is `true`. | + +The binding uses `keepAlive` in its options object. + +## DevTools methods + +The `devtools` property exposes the DevTools JSON endpoints as typed methods. It is one nested binding target, so you can call the methods from the same Browser Run binding: + +| Method | Description | +| -------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `getVersion(sessionId)` | Returns browser version information. | +| `getProtocol(sessionId)` | Returns the browser's DevTools protocol description. | +| `listTargets(sessionId, options?)` | Lists the browser's targets. Set `liveViewUrlExpiresInMs` to control generated Live View URL expiry. | +| `getTarget(sessionId, targetId)` | Returns one target. | +| `newTarget(sessionId, url?, options?)` | Opens a new target. If `url` is omitted, the target opens at `about:blank`. | +| `activateTarget(sessionId, targetId)` | Activates a target. | +| `closeTarget(sessionId, targetId)` | Closes a target. | + +Target objects include the target ID, type, URL, title, and, when available, a `devtoolsFrontendUrl`. + + + +```ts +const targets = await env.BROWSER.devtools.listTargets(sessionId, { + liveViewUrlExpiresInMs: 300_000, +}); + +const page = targets.find((target) => target.type === "page"); +if (!page) { + throw new Error("No page target found"); +} + +const liveView = await env.BROWSER.getLiveView(sessionId, { + targetId: page.id, + mode: "devtools", +}); + +console.log(liveView.devtoolsFrontendUrl); +``` + + + +Use the returned `devtoolsFrontendUrl` to open Live View. To connect a CDP client to a specific target, pass its ID to `connectSession(sessionId, { targetId })`. Treat Live View URLs as credentials because they contain access tokens. + +## Next steps + +- Learn about [Live View](/browser-run/features/live-view/). +- Review [session management with HTTP](/browser-run/cdp/session-management/). +- Use the [Chrome DevTools Protocol](/browser-run/cdp/) for direct browser control. diff --git a/src/content/docs/browser-run/reference/wrangler.mdx b/src/content/docs/browser-run/reference/wrangler.mdx index 87b1b4f839b..07839031300 100644 --- a/src/content/docs/browser-run/reference/wrangler.mdx +++ b/src/content/docs/browser-run/reference/wrangler.mdx @@ -50,6 +50,8 @@ After the binding is declared, access the DevTools endpoint using `env.MYBROWSER const browser = await puppeteer.launch(env.MYBROWSER); ``` +Browser bindings also provide typed methods for [session management, outbound Worker routing, and DevTools operations](/browser-run/reference/browser-binding-api/). + :::note[Quick Actions compatibility] The browser binding's `.quickAction()` method requires a compatibility date of `2026-03-24` or later. Ensure your `wrangler.json` includes: diff --git a/src/content/release-notes/browser-run.yaml b/src/content/release-notes/browser-run.yaml index 1273512de7f..091db406356 100644 --- a/src/content/release-notes/browser-run.yaml +++ b/src/content/release-notes/browser-run.yaml @@ -3,6 +3,11 @@ link: "/browser-run/changelog/" productName: Browser Run productLink: "/browser-run/" entries: + - publish_date: "2026-09-21" + title: "Browser binding session and DevTools methods" + description: |- + * Browser Run browser bindings now support typed methods for [acquiring and connecting to sessions](/browser-run/reference/browser-binding-api/), launching a browser with [outbound Worker routing](/browser-run/features/outbound-workers/), creating Live View URLs, and managing session lifecycle. + * Added a nested `devtools` binding with methods for browser version and protocol information, listing and creating targets, and activating or closing targets. See the [Browser binding API reference](/browser-run/reference/browser-binding-api/) for the full API. - publish_date: "2026-07-28" title: "Structured handoff for Human in the Loop" description: |-