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.
- A Browserless API token from your account dashboard
- The BAP SDK installed and connecting, from the BAP Quickstart
Listen to events
Register listeners before the navigation that produces the events. Anything the page emits before a listener exists is not replayed.
- TypeScript
- Python
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();
}
import bap.sync_api as bap
TOKEN = "YOUR_API_TOKEN_HERE"
with bap.Browserless.connect(
browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
token=TOKEN,
) as browser:
with browser.page() as page:
page.on("console", lambda msg: print(f"[{msg['type']}] {msg['text']}"))
page.on("request", lambda req: print(f"{req['method']} {req['url']}"))
# Response bodies are not fetched by default, since streaming them is expensive.
page.on("response", lambda res: print(f"{res['status']} {res['url']}"))
# Non-fatal streaming and handshake failures surface here instead of raising.
page.on("error", lambda err: print(err))
page.goto("https://example.com")
Response:
GET https://example.com/
200 https://example.com/
[log] hello from the page
The four events
| Event | Payload | Carries |
|---|---|---|
console | ConsoleMessage | type, text, JSON-encoded args, source location, and a timestamp |
request | RequestResponse | url, method, resource type, and headers |
response | ResponseResponse | url, status, method, type, and headers. The body field is empty on this stream, as explained below |
error | Error | Non-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.
- TypeScript
- Python
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));
def on_request(req):
print(req["url"])
page.on("request", on_request)
page.goto("https://example.com")
# Closes the request subscription, since this was the only listener.
page.off("request", on_request)
# Fires once, then removes itself.
page.once("console", lambda msg: print("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()andresponse()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.