Chapter 18Lesson 04~220 minutes

WebDriver BiDi: Events, Network, Logs, Script, and Bidirectional Control: Diagnostics, Failure Modes, and Production Practices

BiDi adds a new transport and event lifecycle, so it adds new failure layers too. This lesson engineers those failures deliberately and diagnoses the smallest layer first instead of retrying, restarting, or silently falling back.

DiagnosticsFailure modesSubscription leaksContext IDsGrid WebSocket

Learning objectives

  • Apply a repeatable diagnostic sequence to BiDi failures.
  • Recognize browser/binding domain mismatch and beta/internal API breakage.
  • Detect leaked subscriptions and races caused by triggering before subscription.
  • Separate BiDi context identifiers from classic window-handle assumptions.
  • Diagnose local versus Grid/WebSocket failures without insecure workarounds.

1. Diagnostic sequence

  1. Preserve the first exception and any event records already captured.
  2. Confirm Selenium, browser, driver/Grid versions.
  3. Confirm loopback target and synthetic test ID.
  4. Inspect session ID, returned capabilities, webSocketUrl, and current context.
  5. Confirm the requested high-level domain exists.
  6. Confirm handler was installed before the trigger and removed afterward.
  7. Inspect AUT/browser/network evidence.
  8. If remote, inspect Grid/WebSocket routing and node state.
  9. Apply the least destructive correction.
  10. Rerun the smallest event scenario.

2. Failure: assuming identical domains across browsers/bindings

Symptom: driver.script works but a network method or event is missing on one lane. Root cause may be real support parity, not a flaky browser. The correction is a versioned capability matrix and a defined fallback/skip policy—not a catch-all retry.

def support_snapshot(driver):
    return {
        "browser": driver.capabilities.get("browserName"),
        "browserVersion": driver.capabilities.get("browserVersion"),
        "webSocketUrl": bool(driver.capabilities.get("webSocketUrl")),
        "script_api": hasattr(type(driver), "script"),
        "network_api": hasattr(type(driver), "network"),
        "browsing_context_api": hasattr(type(driver), "browsing_context"),
    }

print(support_snapshot(driver))

3. Failure: depending directly on beta/internal classes

Symptom: an import, constructor, or event payload shape changes after a Selenium upgrade. If the code imported low-level protocol objects throughout the suite, the blast radius is large. Repair by pinning the current version, moving the dependency behind one adapter, and preferring a documented high-level API when it exists.

Do not solve this by freezing forever

Version pinning is reproducibility, not avoidance. Upgrade through a compatibility lane that exercises the adapter contract.

4. Failure: subscription leak

Symptom: the second test receives two copies of one console message. Typical cause: the first test added a handler but never removed it while the session fixture was reused.

handler_ids = []

def subscribe(driver, sink):
    handler_id = driver.script.add_console_message_handler(sink.append)
    handler_ids.append(handler_id)
    return handler_id

def cleanup(driver):
    while handler_ids:
        driver.script.remove_console_message_handler(handler_ids.pop())

Production fixtures should make cleanup failure visible. Swallowing remove-handler exceptions can conceal a contaminated shared session.

5. Failure: event fires before subscription is active

Broken sequence: navigate, click, then install handler. The console event already happened and BiDi does not replay it merely because a listener appears later.

# Intentionally broken
# driver.find_element(By.CSS_SELECTOR, '[data-testid="console"]').click()
# handler_id = driver.script.add_console_message_handler(messages.append)

# Repaired
handler_id = driver.script.add_console_message_handler(messages.append)
driver.find_element(By.CSS_SELECTOR, '[data-testid="console"]').click()
WebDriverWait(driver, 3).until(lambda _: messages)

6. Failure: mixing classic window handles and BiDi context IDs

Symptom: a context-scoped subscription produces no events or targets the wrong page after a new tab/frame appears. The diagnostic record should label classic_window_handle and bidi_context_id separately. Discover/record the BiDi context through the BiDi browsing-context API when the use case requires context scoping; do not rely on accidental identifier equality.

7. Failure: CDP-only code presented as BiDi

Symptom: a test passes on Chrome and fails structurally on Firefox because it calls execute_cdp_cmd(). This is not a BiDi parity defect; the test architecture selected a Chromium protocol. Repair by moving to the supported BiDi domain or labeling the test Chromium-only.

8. Failure: classic Grid works, BiDi WebSocket fails

Symptom: normal WebDriver commands succeed remotely but event handlers cannot connect or never receive events. Preserve the session ID and returned WebSocket URL, then inspect Grid/router/node versions, advertised endpoint reachability, reverse-proxy WebSocket upgrade support, and ingress/firewall rules. Do not expose Grid publicly, disable TLS verification, or restart the entire Grid as a first response.

9. Intentionally broken example and interpretation

The following example makes the Intentionally broken example and interpretation behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

from selenium import webdriver
from selenium.webdriver.common.by import By

options = webdriver.ChromeOptions()
options.enable_bidi = True
driver = webdriver.Chrome(options=options)
messages = []
try:
    driver.get("http://127.0.0.1:8782/?test_id=broken")
    driver.find_element(By.CSS_SELECTOR, '[data-testid="console"]').click()
    handler_id = driver.script.add_console_message_handler(messages.append)  # too late
    assert messages, "No BiDi message captured"
finally:
    driver.quit()

The assertion is expected to fail because the observer was installed after the causal action. Increasing the timeout cannot recover a past event. The least destructive correction is ordering: subscribe first, trigger second, wait third, unsubscribe fourth.

10. Performance diagnosis

If an event-heavy suite slows down, separate callback processing time from browser startup, AUT/network latency, Grid queue time, and artifact I/O. A broad subscription may generate thousands of events; reducing scope can fix the capacity problem without touching browser timeouts.

11. Production runbook

  1. Freeze first-failure evidence.
  2. Record support snapshot.
  3. Check subscription order and IDs.
  4. Check context scope.
  5. Check browser/AUT event actually occurred.
  6. For remote runs, check WebSocket routing.
  7. Classify: unsupported feature, test lifecycle defect, browser defect, Grid transport incident, or AUT behavior.
  8. Repair the smallest layer and rerun one scenario.

12. Lesson summary

  • Support mismatch is not flakiness.
  • Internal API churn is contained with adapters and version pins.
  • Subscriptions can leak and races can miss events.
  • Context identifiers must remain semantically labeled.
  • CDP failures are not cross-browser BiDi failures.
  • Remote BiDi adds WebSocket routing as a distinct infrastructure layer.

Knowledge check

A console handler receives duplicate events only on the second test in a reused session. First hypothesis?

Why will a longer wait not repair a handler installed after the event?

Classic Grid commands work but BiDi events do not. What layer becomes a prime suspect?

A Firefox test fails because it calls execute_cdp_cmd(). Is that a BiDi browser-parity bug?

Why record classic and BiDi context identifiers under different keys?

Next lesson

Checkpoint Lab — WebDriver BiDi: Events, Network, Logs, Script, and Bidirectional Control

Continue with Checkpoint Lab — WebDriver BiDi: Events, Network, Logs, Script, and Bidirectional Control. It builds directly on the state, evidence, and operating assumptions established here, so carry those constraints forward rather than treating the next page as an isolated topic.

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.