WebDriver BiDi: Events, Network, Logs, Script, and Bidirectional Control: Core Concepts and Mental Model
Classic WebDriver is excellent at issuing a command and receiving its result. It is less natural when the browser needs to tell the test runner that something happened asynchronously. WebDriver BiDi adds that event/control channel. This lesson builds the protocol and ownership model before learners subscribe to anything.
Learning objectives
- Distinguish classic WebDriver request/response traffic from the BiDi WebSocket event channel.
- Define browsing contexts, event domains, subscriptions, callbacks, and handler lifetime.
- Inspect negotiated BiDi capability and session state before using event APIs.
- Prefer documented high-level Selenium BiDi APIs over low-level/internal transport classes.
- Explain why CDP is Chromium-specific and temporary rather than a cross-browser BiDi substitute.
1. Why classic WebDriver needs a companion channel
A command such as find_element() or
click() has a clear request and response. Console
messages, network requests, JavaScript exceptions, prompt events,
and navigation events arrive whenever the browser produces them.
Polling repeatedly can miss short-lived events, adds latency, and
couples tests to timing guesses. BiDi gives the browser a
standards-based path to push subscribed events back to the client.
BiDi complements classic WebDriver. It does not replace locators, ordinary interactions, waits, session ownership, or assertions.
2. Mental model: classic commands plus bidirectional events
The following diagram visualizes the relationships described in Mental model: classic commands plus bidirectional events. Read the nodes in sequence and use the arrows to connect the conceptual state changes to the explanation around the diagram.
flowchart TD T[Test runner] -->|Classic command| D[WebDriver / Grid] D -->|Command response| T D <-->|BiDi WebSocket| B[Browser] B -->|log / network / script events| T T -->|subscribe / unsubscribe| B B --> A[AUT] A -->|console / fetch / navigation| B T --> E[Correlated evidence]
The classic path remains synchronous request/response. When BiDi is
enabled, the negotiated session can also expose a WebSocket URL.
Selenium layers user-facing domain objects such as
driver.script, driver.network, and
driver.browsing_context on that channel. A subscription
tells the browser which event family matters. A callback receives
matching events until the handler is removed or the session ends.
3. Objects and state stores
The following table organizes the key choices and evidence for Objects and state stores. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.
| Object | Meaning | State it owns | Failure if confused |
|---|---|---|---|
| WebDriver session | One negotiated automation session | session ID, capabilities, windows, cookies | Opening a second driver is a second session, not another BiDi channel for the first |
| Classic window handle | Identifier used by classic top-level context commands | current classic browsing target | Treating every handle as an arbitrary BiDi context ID hides semantic boundaries |
| BiDi browsing context | W3C BiDi target such as a top-level context or frame | context tree and context-scoped events | Subscribing to the wrong context yields missing or irrelevant events |
| Subscription / handler ID | Lifecycle token returned by a high-level API | which callback is active | Leak causes duplicate callbacks and cross-test contamination |
| Domain | Related commands/events such as log, network, script | event schemas and controls | Assuming every binding/browser implements identical domains |
| WebSocket URL | Negotiated endpoint for BiDi transport | transport route for this session | Copying or exposing remote endpoints can leak operational access |
| Grid | Optional remote session router/node system | session placement and WebSocket proxying | Network reachability can fail even when classic HTTP commands work |
4. Read-only preflight before enabling listeners
The following example makes the Read-only preflight before enabling listeners behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
import selenium
from selenium import webdriver
options = webdriver.ChromeOptions()
options.enable_bidi = True
driver = webdriver.Chrome(options=options)
try:
caps = driver.capabilities
print("selenium", selenium.__version__)
print("session", driver.session_id)
print("browser", caps.get("browserName"), caps.get("browserVersion"))
print("platform", caps.get("platformName"))
print("webSocketUrl", caps.get("webSocketUrl"))
print("classic_handle", driver.current_window_handle)
finally:
driver.quit()
options.enable_bidi = True asks Selenium to negotiate
BiDi. The returned webSocketUrl capability proves
whether that session actually exposes the transport. Record
browser/binding versions and the negotiated value rather than
assuming support from the browser name alone.
5. Events and command domains
BiDi is organized into domains. Logging use cases are surfaced by
Selenium through the script namespace, network
interception/observation through network, and
browsing-context control through browsing_context. The
protocol itself contains lower-level commands and event types, but
Selenium explicitly encourages high-level use-case APIs where
available.
| Domain | Typical use | State mutation? | Evidence risk |
|---|---|---|---|
| script / log | console messages, JS errors, script operations | handlers mostly observe; script commands can mutate page state | console may contain user data/tokens |
| network | request/response observation, auth, interception | observation can be passive; interception mutates request flow | headers/bodies can be highly sensitive |
| browsing context | create/navigate/capture/observe context lifecycle | yes for navigation/create/close | context metadata and URLs |
| input | low-level BiDi input commands | yes | can change AUT state |
| browser | browser/user-context operations | yes | profile-like isolation and policy state |
6. Subscription lifecycle is resource lifecycle
An event handler is not a free-standing callback. It is session-scoped state. Add it before the triggering action, preserve the returned handler/subscription identifier, and remove it in cleanup. If a fixture reuses a browser session, an unremoved handler can fire in later tests and create duplicate evidence.
events = []
handler_id = driver.script.add_console_message_handler(events.append)
try:
# trigger only after the handler exists
pass
finally:
driver.script.remove_console_message_handler(handler_id)
7. Browsing context is not merely “the current tab”
BiDi event APIs can scope subscriptions to browsing contexts. A context identifier has protocol meaning: it identifies a context in the BiDi context tree. Classic WebDriver window handles identify top-level contexts for classic commands. Current Selenium may bridge these concepts in convenient APIs, but production code should still label the identifier type explicitly instead of passing opaque strings between layers and hoping they are interchangeable.
8. High-level APIs versus low-level/internal classes
The low-level BiDi protocol surface evolves quickly. Selenium
documentation states that low-level components remain accessible
but are not the recommended user-facing layer. Pin the binding
version and prefer stable high-level methods such as
driver.script.add_console_message_handler().
The following table organizes the key choices and evidence for High-level APIs versus low-level/internal classes. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.
| Choice | Benefit | Cost | Recommended use |
|---|---|---|---|
| High-level Selenium API | portable intent, simpler lifecycle | may not expose newest protocol feature immediately | default |
| Low-level protocol class | earlier access to new domain detail | beta/internal churn, schema coupling | small isolated adapter only when necessary |
| Classic polling | widely understood, no BiDi dependency | can miss events or add delay | state conditions that are naturally queryable |
| CDP | deep Chromium-specific control | not cross-browser; temporary Selenium bridge | only explicitly Chromium-specific requirements |
9. BiDi versus CDP
Chrome DevTools Protocol is a Chromium protocol. Selenium has historically exposed CDP to bridge gaps while WebDriver BiDi matured. Current Selenium documentation describes CDP support as temporary, and Selenium 4.47 blocks Firefox CDP access in Python. A test that calls Chromium CDP and then labels itself “cross-browser BiDi” is architecturally incorrect.
10. Trust boundaries and privacy
BiDi can reveal rich telemetry. Console messages may contain tokens; network handlers can see URLs, headers, cookies, and possibly bodies; remote WebSocket endpoints are session infrastructure. Use synthetic data in training, redact before persistence, minimize subscriptions, and never expose public Grid/BiDi endpoints. Event richness increases privacy responsibility.
11. DevOps connection: observability with version discipline
BiDi can turn opaque CI failures into event-rich incidents, but only if the pipeline records the exact Selenium binding, browser version, returned capabilities, selected domains, handler lifecycle, and any fallback used. A green test produced after silently disabling unsupported evidence is not equivalent to a fully observed run.
12. Lesson summary
- Classic WebDriver issues commands; BiDi adds asynchronous browser-to-client events and bidirectional control.
- Enablement is negotiated per session and should be proven by returned capability state.
- Subscriptions are resources that must be created before the trigger and removed deterministically.
- Browsing-context identifiers, classic handles, domains, and WebSocket endpoints are distinct concepts.
- Prefer high-level Selenium BiDi APIs; isolate low-level version-sensitive code.
- CDP is browser-specific and temporary, not cross-browser BiDi.
Knowledge check
What proves that a newly created session actually negotiated BiDi?
The returned session capabilities should expose a BiDi WebSocket
URL (for example webSocketUrl), together with the
exact browser/binding version.
Why must a handler be installed before the action that emits the event?
BiDi events are asynchronous and not retroactive; subscribing afterward can miss the causal event.
Is a classic window handle conceptually identical to every BiDi browsing-context ID?
No. Keep identifier types explicit and use the API contract for the context being addressed instead of treating opaque strings as universally interchangeable.
Why is CDP not a valid cross-browser fallback for Firefox in this chapter?
CDP is Chromium-specific; Selenium 4.47 also blocks Firefox CDP access in Python, reinforcing BiDi as the standards path.
What is the cleanup responsibility for an event subscription?
Remove the handler/subscription deterministically before session teardown or fixture reuse so later tests cannot receive duplicate or stale callbacks.
Official references and version notes
- Selenium 4.47 release notes — current stable client/Grid baseline and BiDi changes.
- WebDriver BiDi overview — high-level Selenium direction and CDP relationship.
- W3C-compliant BiDirectional API — cross-browser WebSocket event model and domains.
- BiDi logging features — console and JavaScript error handlers.
- BiDi network features — request/response/auth handler concepts.
- BiDi script features — high-level script namespace.
-
Python Options API
—
enable_bidi. - Selenium Python API 4.47 — Python 3.10+ and current binding surface.
- Selenium Grid — remote sessions and the next chapter boundary.
Version-sensitive behavior was rechecked against Selenium primary
documentation on 2026-08-28. Mandatory examples pin Selenium
Python 4.47.0 and Python 3.10+, request BiDi through
options.enable_bidi = True, prefer documented
high-level driver.script/driver.network
APIs, use Selenium Manager for normal local driver resolution, and
target only the loopback synthetic AUT. BiDi domain parity is
explicitly treated as evolving. Low-level/internal classes are
discussed as an adapter-only escape hatch, not the default. CDP is
labeled Chromium-specific/temporary and is not used as a
cross-browser fallback. Paid clouds, enterprise identity/proxies,
managed Kubernetes, and public Grid endpoints are not required.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.