For AI agents: a documentation index is available at /llms.txt
Skip to main content

Page events in BAP

page.on() streams console output, network requests, and responses as the page produces them, using the same on/once/off shape as Puppeteer and Playwright. This guide covers the four event types, what each payload carries, and the subscription lifecycle that makes listeners cost something.

Prerequisites

Listen to events

Register listeners before the navigation that produces the events. Anything the page emits before a listener exists is not replayed.

import Browserless from "@browserless.io/bap-ts";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
token: TOKEN,
});

try {
const page = await browser.newPage();

page.on("console", (msg) => console.log(`[${msg.type}] ${msg.text}`));
page.on("request", (req) => console.log(`${req.method} ${req.url}`));

// Response bodies are not fetched by default, since streaming them is expensive.
page.on("response", (res) => console.log(`${res.status} ${res.url}`));

// Non-fatal streaming and handshake failures surface here instead of throwing.
page.on("error", (err) => console.error(err.message));

await page.goto("https://example.com");
} finally {
await browser.close();
}

Response:

GET https://example.com/
200 https://example.com/
[log] hello from the page

The four events

EventPayloadCarries
consoleConsoleMessagetype, text, JSON-encoded args, source location, and a timestamp
requestRequestResponseurl, method, resource type, and headers
responseResponseResponseurl, status, method, type, and headers. The body field is empty on this stream, as explained below
errorErrorNon-fatal streaming and handshake failures that would otherwise be invisible

The error event is the one worth wiring up even when you don't need the others. Failures in the subscription stream itself don't reject any of your pending calls, so without a listener they pass silently.

Subscriptions open and close with your listeners

Listeners aren't free. Attaching the first console, request, or response listener opens a GraphQL subscription for that event type, and removing the last one closes it. That's why the API has once() and off(): a listener you forget to remove keeps a stream running for the life of the page.

const onRequest = (req) => console.log(req.url);

page.on("request", onRequest);
await page.goto("https://example.com");

// Closes the request subscription, since this was the only listener.
page.off("request", onRequest);

// Fires once, then removes itself.
page.once("console", (msg) => console.log("first message:", msg.text));

Two more methods help when you're managing listeners in longer-lived code: listenerCount(event) reports how many are attached, and removeAllListeners(event) clears them for one event, or for every event when called with no argument. Both are useful in cleanup paths, since clearing the last listener is what closes the underlying stream.

Response bodies are excluded from the response stream by default because streaming every body is expensive. When you need bodies, query the captured traffic with response() instead, which lets you filter down to the handful of responses you care about.

Events versus querying traffic

Both surfaces see network activity, and they suit different jobs:

  • Events are push-based and live. Use them to log or react as things happen, especially when you don't know in advance which requests will matter.
  • request() and response() are pull-based queries with server-side filters. Use them when you know what you're looking for, need response bodies, or want to check what already happened after a navigation.

FAQ & Troubleshooting

My listener never fires

It was probably attached after the events happened. Register listeners before goto() or before the interaction that triggers the traffic, since nothing is replayed to a late listener.

The response event has no body

Bodies are omitted by design, because streaming every response body is expensive. Fetch the ones you need with response() from the network control guide, filtering by URL or status.

Can I filter events, the way Puppeteer lets me inspect and decide?

Not on the subscription itself. Filter inside your listener, or use request()/response() where the filters run server-side and less data crosses the wire.

Is there an event for raw protocol frames?

Yes, in TypeScript: page.on('message') and page.subscribe() expose every transport frame, including server pushes. See raw BrowserQL.

Next steps

Was this page helpful?