Resource library

QA How-To

Selenium BiDi network in Python (2026)

Learn selenium BiDi network python setup for request capture, interception, authentication, failure testing, cleanup, and reliable CI test checks in 2026.

23 min read | 2,912 words

TL;DR

Set options.enable_bidi = True, create the driver, and use driver.network.add_request_handler with the before_request event. The callback receives a Request object, and it must call continue_request() or fail_request() before returning.

Key Takeaways

  • Enable BiDi before creating the driver by setting options.enable_bidi to True.
  • Register a before_request handler before navigation so early document requests are not missed.
  • Every intercepted request must be continued, failed, or otherwise resolved to prevent a stalled page.
  • Use URL checks inside callbacks to limit collection and mutation to traffic owned by the test.
  • Store handler IDs and remove handlers during teardown to prevent cross-test contamination.
  • Prefer WebDriver BiDi over browser-specific CDP code when the required high-level feature is available.

The selenium BiDi network python workflow gives a test direct, event-driven control over browser requests through the WebDriver BiDi connection. In Selenium's current Python API, you enable BiDi on the browser options, register a handler through driver.network, and decide whether each matching request should continue, change, or fail.

This is useful for more than printing URLs. A focused network assertion can prove that a UI action called the correct API, a request interceptor can add test-only headers, and deliberate failure injection can verify a real error path. This guide uses Selenium's public high-level network API and calls out the boundaries where the Python binding does not yet offer the same convenience for response inspection.

TL;DR

Goal Current Python API Essential rule
Enable a BiDi session options.enable_bidi = True Set it before driver creation
Intercept outgoing traffic driver.network.add_request_handler('before_request', callback) Register before navigation
Let a request proceed request.continue_request() Resolve every intercepted request
Simulate a network failure request.fail_request() Restrict the failure to an exact target
Handle browser authentication driver.network.add_auth_handler(user, password) Remove the handler after the scenario
Stop interception remove_request_handler(event, id) Keep the returned handler ID

1. Selenium BiDi network python fundamentals

WebDriver BiDi is the bidirectional transport defined for browser automation. Traditional WebDriver commands follow a request and response pattern: the client asks the browser to navigate, click, or read state, then waits for a result. BiDi adds a WebSocket connection through which the browser can send events while the session is running. Network activity, console messages, JavaScript errors, and browsing context events fit naturally into this model.

Selenium wraps the protocol with high-level features. For Python network work, driver.network returns a Network object. Its request handler pauses matching traffic at a supported interception phase and supplies a Request object to a callback. The useful fields include url, headers, cookies, resource_type, body_size, timings, and request_id. Availability can vary by event and browser, so production assertions should tolerate optional metadata while treating the URL and intended action as the primary contract.

BiDi is not the same thing as Chrome DevTools Protocol. CDP is Chromium-specific and versioned with Chromium. WebDriver BiDi is designed as a cross-browser standard and is the direction to prefer for portable suites. Selenium may still expose CDP escape hatches, but new framework code should start with the public BiDi feature that meets the need. If you are reviewing a broader migration, compare the tradeoffs in Selenium vs Cypress for modern automation.

2. Install Selenium and enable WebDriver BiDi

Use a current Selenium 4 release and a supported Python runtime. Selenium Manager is included with Selenium, so a normal local setup usually does not need a separately downloaded driver binary. Pin the Selenium version in your project lock file or requirements file so local machines and CI execute the same binding.

selenium==4.46.0
pytest>=8.0

The network property needs a WebDriver BiDi WebSocket URL. Ask the browser for that capability by enabling BiDi on its options before the session starts.

from selenium import webdriver

options = webdriver.ChromeOptions()
options.enable_bidi = True

driver = webdriver.Chrome(options=options)
try:
    assert driver.capabilities.get('webSocketUrl')
finally:
    driver.quit()

The same enable_bidi option exists on Selenium option classes for browsers that support BiDi. Browser implementation coverage still matters. Run a small capability smoke test on every browser and Grid image that your organization claims to support, rather than assuming that identical client methods guarantee identical event coverage.

Do not confuse enable_bidi with performance logging capabilities such as goog:loggingPrefs. Performance logs are a Chromium integration that returns buffered DevTools messages. BiDi handlers are live event subscriptions. They differ in portability, data shape, timing, and cleanup behavior.

3. Capture outgoing requests with a runnable handler

The following program is a complete request-capture example. It registers the callback before navigation, records only Selenium documentation URLs, continues every request, waits for evidence, then removes the handler. The callback is intentionally short because it runs on Selenium's event-processing path.

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait

options = webdriver.ChromeOptions()
options.enable_bidi = True

driver = webdriver.Chrome(options=options)
seen_urls: list[str] = []

def capture(request) -> None:
    if request.url and 'selenium.dev' in request.url:
        seen_urls.append(request.url)
    request.continue_request()

handler_id = driver.network.add_request_handler(
    'before_request',
    capture,
)

try:
    driver.get('https://www.selenium.dev/')
    WebDriverWait(driver, 10).until(lambda _driver: len(seen_urls) > 0)
    assert any(url.startswith('https://www.selenium.dev/') for url in seen_urls)
finally:
    driver.network.remove_request_handler('before_request', handler_id)
    driver.quit()

Calling continue_request() is not a courtesy. Adding a request handler creates an intercept, so traffic pauses while the callback decides what to do. A callback that merely appends a URL and returns can leave the navigation waiting until a timeout. Put the continuation in a finally block if the observation code could raise.

The handler sees much more traffic than a user notices. Documents, stylesheets, fonts, images, fetch calls, and background requests may all pass through it. Collect only what the test needs, and assert on a known endpoint or correlation marker instead of the total number of requests.

4. Filter traffic and make deterministic assertions

A network test becomes flaky when it treats all page traffic as stable. Analytics, feature flags, content delivery hosts, preloads, service workers, and browser updates can alter the request list without changing the feature under test. Start with a narrow business question, such as: did clicking Save issue a PATCH request to the profile endpoint? Then make the handler collect evidence for that question alone.

The current high-level Request object exposes optional fields. Check for None before using them. In particular, do not build a critical assertion around resource_type across all browsers until your own support matrix proves it is consistently populated. URL matching is usually the most portable first filter. Path parsing is safer than loose substring checks when similarly named endpoints exist.

from urllib.parse import urlparse

matched: list[str] = []

def capture_profile_request(request) -> None:
    try:
        if request.url:
            parsed = urlparse(request.url)
            if parsed.netloc == 'app.example.test' and parsed.path == '/api/profile':
                matched.append(request.url)
    finally:
        request.continue_request()

After the UI action, use WebDriverWait or another bounded wait for the collection to contain the expected event. Avoid time.sleep(). The event may arrive immediately on one machine and later on a busy CI node. Also avoid asserting that the request list has exactly one item unless the application contract guarantees it. Retry logic can legitimately issue more than one call. For more API-focused strategies, see API error handling and negative testing.

5. Selenium BiDi network python request mutation

Request mutation is valuable when a test environment recognizes a header, routes a request to a stub, or needs a temporary test identity. The callback can pass replacement values to continue_request. Preserve the existing headers and append only the value your test owns. BiDi represents header values as typed byte values, so use the protocol-shaped dictionary shown below.

def add_test_run_header(request) -> None:
    headers = list(request.headers or [])
    headers.append({
        'name': 'x-test-run',
        'value': {'type': 'string', 'value': 'checkout-smoke'},
    })
    request.continue_request(headers=headers)

handler_id = driver.network.add_request_handler(
    'before_request',
    add_test_run_header,
)

In a real suite, filter before mutating. Adding a private header to every third-party request can trigger cross-origin preflights, leak internal labels, or change application behavior that the test did not intend to exercise. A safer callback checks the exact host and path, mutates matching traffic, and calls plain continue_request() for everything else.

The same method accepts optional body, method, cookies, and url arguments. Treat those as deliberate test doubles, not casual shortcuts. Changing a method from GET to POST can invalidate cache assumptions and server routing. Replacing a body requires the correct BiDi byte-value representation and the matching content headers. Keep mutations small, document why they exist, and add a server-side assertion when possible so the test proves the change arrived.

6. Fail selected requests and test recovery behavior

A UI error state deserves an end-to-end test that creates a real transport failure. request.fail_request() tells the browser to fail an intercepted request. This is different from returning an HTTP 500 response. A failed request represents a network-level failure, while a 500 proves how the application handles a completed HTTP response with an error status. Cover both when the product distinguishes them.

failed_urls: list[str] = []

def fail_recommendations(request) -> None:
    if request.url and request.url.endswith('/api/recommendations'):
        failed_urls.append(request.url)
        request.fail_request()
    else:
        request.continue_request()

handler_id = driver.network.add_request_handler(
    'before_request',
    fail_recommendations,
)

The branch must resolve every request exactly once. Do not call continue_request() after fail_request(), and do not let an unmatched request fall through without continuation. An exact path, dedicated test host, or test-controlled query parameter reduces the chance of failing an unrelated asset.

Assert on user-visible recovery, not only on failed_urls. A strong test checks that a useful error message appears, the primary page remains usable, a retry action is available, and retrying succeeds after the handler is removed. This links the injected fault to customer behavior. Network evidence alone proves the interceptor ran, not that the product responded correctly.

7. Handle HTTP authentication with the high-level API

Browser authentication prompts are difficult to automate through the page DOM because they are browser UI, not HTML. Selenium's network feature provides an authentication handler that supplies a username and password when the BiDi authRequired event occurs. This is appropriate for Basic or Digest authentication supported by the browser flow. It is not a replacement for filling an application login form.

from selenium import webdriver

options = webdriver.ChromeOptions()
options.enable_bidi = True
driver = webdriver.Chrome(options=options)

auth_id = driver.network.add_auth_handler('admin', 'correct-horse-battery-staple')
try:
    driver.get('https://the-internet.herokuapp.com/basic_auth')
    assert 'Congratulations' in driver.page_source
finally:
    driver.network.remove_auth_handler(auth_id)
    driver.quit()

Never hardcode real secrets in a repository. Read test credentials from the CI secret store, keep them scoped to a disposable environment, and prevent them from appearing in callback logging or failure attachments. The literal above is only sample data for a public demonstration endpoint.

Authentication handlers are session state. Remove them as soon as the scenario ends. If a broad fixture leaves a handler active, later requests can receive credentials unexpectedly and tests can pass for the wrong reason. A function-scoped fixture with cleanup is usually safer than a session-scoped network fixture.

8. Design a reusable pytest fixture without leaking handlers

Framework code should own the lifecycle that individual tests can easily forget. A pytest fixture can enable BiDi, create the driver, and guarantee that all request handlers are cleared before quitting. Tests can still register specialized callbacks and retain their handler IDs when they need early removal.

import pytest
from selenium import webdriver

@pytest.fixture
def bidi_driver():
    options = webdriver.ChromeOptions()
    options.enable_bidi = True
    driver = webdriver.Chrome(options=options)
    try:
        yield driver
    finally:
        driver.network.clear_request_handlers()
        driver.quit()

def test_checkout_calls_pricing(bidi_driver):
    pricing_calls = []

    def capture(request):
        try:
            if request.url and '/api/pricing' in request.url:
                pricing_calls.append(request.url)
        finally:
            request.continue_request()

    bidi_driver.network.add_request_handler('before_request', capture)
    bidi_driver.get('https://app.example.test/checkout')
    # Trigger checkout behavior here, then wait for pricing_calls.

Do not turn this fixture into a global network recorder. Large unbounded lists increase memory use and make failure output noisy. Prefer a small per-test collector, a predicate, and a bounded queue if traffic can be high. If callbacks write to shared state while the test reads it, use thread-safe containers or a lock for compound operations. A simple list append is often enough for a small CPython test, but framework utilities should make their concurrency assumptions explicit.

9. Compare BiDi, performance logs, proxies, and API clients

No single network tool is best for every QA question. Choose the smallest mechanism that proves the risk.

Approach Best use Portability Main limitation
Selenium WebDriver BiDi Live browser request interception and auth Designed for multiple browsers High-level binding coverage is still growing
Chromium performance log Buffered Chromium DevTools events Chromium only Messages require parsing and reads drain the buffer
Selenium CDP integration Chromium-specific DevTools commands Chromium only Tied to CDP versions and intended to be replaced by BiDi
External proxy Full HTTP capture, response stubbing, certificates Browser-independent in principle Operational setup and TLS trust are more complex
Direct API client Contract and service assertions Not tied to a browser Does not prove browser behavior or UI integration

Use BiDi when the browser event is part of the behavior you need to prove. Use an API test when the question is purely about response schema, status, or business rules. Use a proxy when your required capture or response modification is beyond the current high-level binding and the infrastructure cost is justified.

The Python network API is strongest today for request interception and authentication. Do not invent a convenient response-body method because another language or a low-level module has one. If response capture is mandatory, confirm the current binding's supported public feature, or use a deliberate alternative. This discipline is especially important in tutorials, where copied pseudo-APIs become production defects.

10. Make network tests reliable on Grid and CI

Remote execution adds latency, container boundaries, browser version drift, and parallel sessions. None of those require a different callback API, but they expose weak assumptions. Enable BiDi in the options sent to Grid and verify that the returned capabilities contain a WebSocket URL. Keep the Selenium client, Grid, browser, and driver images compatible, then pin the image identifiers used by CI.

A callback runs for one driver session. Never share a driver's network object between tests or threads. In parallel execution, attach a unique run ID to test-owned traffic and store evidence under that test's artifact directory. Avoid printing every header because authorization tokens and cookies can appear there. Redact secrets before persisting diagnostics.

Use bounded waits for both UI state and network evidence. If the UI succeeds but the expected request is absent, report the expected host and path plus a sanitized sample of observed URLs. That message is far more useful than a raw timeout. If the callback never fires, report the browser name, version, WebSocket capability, and Grid node image. The Docker guide for Selenium Grid explains how reproducible browser containers reduce environment drift.

Finally, keep network assertions close to product contracts. An endpoint rename should cause one intentional framework or test update, not dozens of substring edits. A small endpoint catalog or page-level helper can centralize those contracts without hiding the reason each test observes traffic.

Interview Questions and Answers

Q: What is the difference between WebDriver BiDi and classic WebDriver?

Classic WebDriver is primarily command and response based. WebDriver BiDi adds a bidirectional WebSocket channel, so the browser can publish events such as network activity and console output while the session runs. Selenium exposes high-level APIs on top of that connection. Tests still use normal WebDriver commands for page interaction. The event stream should be treated as session-scoped state: subscribe before the behavior, retain only evidence needed by the scenario, and unsubscribe as soon as the observation window closes. This keeps a BiDi feature composable with ordinary page objects instead of turning it into a global recorder.

Q: Why must a before_request callback call continue_request()?

The handler creates an interception point that pauses matching traffic. continue_request() resolves the intercept and lets the browser send the request. Without a continue or fail decision, navigation or application behavior can stall. Cleanup does not substitute for resolving the current request.

Q: When would you call fail_request() instead of mocking a 500 response?

Use fail_request() to exercise a transport failure such as an unavailable network path. Use a 500 response to test a completed HTTP exchange whose server reported an error. Applications can handle those cases differently, so mature resilience coverage usually treats them as separate scenarios.

Q: How do you avoid flaky Selenium network assertions?

Filter by a stable host and exact path, register before the action, wait for a specific event with a timeout, and avoid total request-count assertions. Resolve every intercepted request and remove handlers after the test. The final assertion should connect the network evidence to visible product behavior. I also separate observation from control: one callback records a narrow request contract, while a different test owns failure injection or mutation. That separation prevents a diagnostic listener from silently changing traffic and makes failures easier to attribute during review.

Q: Is WebDriver BiDi the same as Chrome DevTools Protocol?

No. CDP is Chromium's debugging protocol, while WebDriver BiDi is a browser automation standard intended for interoperable implementations. Selenium supports both in some contexts, but BiDi is the preferred direction for portable new features. Support still needs to be verified against each target browser.

Q: What should a framework log when a BiDi network test fails?

Log sanitized target URLs, the expected endpoint, browser and driver versions, session capabilities relevant to BiDi, and the test correlation ID. Do not dump cookies, authorization headers, or complete request bodies by default. Diagnostics should explain the missing evidence without creating a secret-management incident.

Q: How would you test a retry flow with network interception?

Fail only the target request, assert that the retry UI appears, remove the failure handler, then trigger retry. Wait for the success state and, if useful, capture the successful request with a separate observer. This proves both recovery messaging and the actual retry behavior.

Common Mistakes

  • Enabling BiDi after the driver has already been created. The capability must be requested during session creation.
  • Registering the handler after navigation or after the click that triggers the request. Early traffic cannot be recovered later.
  • Recording an intercepted request without continuing or failing it. This commonly appears as a page-load timeout rather than an obvious callback error.
  • Matching a broad substring such as api and mutating unrelated traffic. Match a known host and path.
  • Assuming every Request field is populated on every browser. Guard optional metadata and test the claimed support matrix.
  • Sharing one driver or callback collection across parallel tests. Keep session state and evidence isolated.
  • Logging raw headers, cookies, or bodies into CI artifacts. Sanitize before persistence.
  • Treating network evidence as the only assertion. Also verify the user-visible outcome that matters.
  • Reaching into selenium.webdriver.common.bidi.* low-level implementation classes when a high-level driver.network feature exists. Internal protocol mappings can change.
  • Forgetting to remove handlers. Leaked intercepts create order-dependent tests that are difficult to diagnose.

For broader preparation on explaining these tradeoffs, work through Selenium interview questions for experienced testers.

Conclusion

A sound selenium BiDi network python implementation starts with options.enable_bidi = True, registers a narrowly scoped before_request callback, and resolves every intercepted request. From there, request capture, header injection, transport-failure testing, and browser authentication become concise additions to a normal Selenium test.

Keep the callback small, the assertions product-focused, and the handler lifecycle explicit. Build one smoke test against your supported browsers and Grid first, then extract a fixture only after the behavior is proven. That gives the suite useful network control without turning every UI test into a fragile packet recorder.

Interview Questions and Answers

What problem does WebDriver BiDi solve for Selenium network testing?

It gives the browser a bidirectional event channel instead of relying only on command and response calls. Selenium can receive live network events and pause requests through that channel. This supports event-driven observation and interception without polling a browser-specific log buffer.

What is the lifecycle of a before_request handler in Selenium Python?

Enable BiDi during session creation, register the handler before the triggering action, and save the returned ID. The callback resolves each intercepted request by continuing or failing it. Remove the handler in teardown so it cannot affect later scenarios.

How would you make a network assertion deterministic?

Match an exact business endpoint, use a bounded wait for that event, and avoid depending on unrelated assets or total traffic counts. Keep the callback short and protect optional fields. Finally, assert the visible feature outcome as well as the captured request.

When should an SDET prefer an API test over Selenium BiDi?

Prefer an API test when the risk is entirely in status codes, schemas, or service business rules and browser behavior adds no value. Use BiDi when you need to prove that a real UI action emitted or reacted to browser traffic. The layers complement each other rather than compete.

What security risks exist when capturing browser network traffic?

Headers, cookies, URLs, and bodies can contain credentials, personal data, and tokens. Collect only what the assertion needs, redact diagnostics, isolate artifacts by test, and keep test credentials in a secret store. Broad debug logging should never be the default in CI.

How do BiDi and CDP differ in a Selenium framework?

BiDi is a standards-based browser automation protocol intended for multiple browser implementations. CDP is Chromium's debugging protocol and follows Chromium versions. New portable framework features should prefer Selenium's high-level BiDi APIs when they cover the requirement.

How would you diagnose a request handler that never fires on Grid?

First confirm that BiDi was enabled before session creation and that returned capabilities include a WebSocket URL. Record the Selenium client, Grid, browser, and driver versions, then run a minimal known-URL smoke test. Also verify that the handler was installed before navigation and that the Grid node exposes the required BiDi support.

Frequently Asked Questions

How do I enable Selenium BiDi in Python?

Create the browser options, set options.enable_bidi = True, and pass those options when creating the driver. The returned session capabilities should include a webSocketUrl when the browser and remote end support BiDi.

Can Selenium capture network requests in Python without Chrome performance logs?

Yes. The public driver.network API uses WebDriver BiDi and can register request handlers for supported interception phases. It is a better starting point for portable new code than Chromium-only performance logs.

Why does my page hang after adding a Selenium network handler?

A request handler intercepts and pauses matching traffic. Every callback path must call request.continue_request(), request.fail_request(), or another valid resolution, otherwise the browser can remain blocked.

Can Selenium BiDi modify request headers in Python?

Yes. Pass a complete replacement header collection to request.continue_request(headers=...). Preserve the original headers, use the BiDi typed value shape, and restrict changes to a test-owned host and path.

Does Selenium BiDi work with Firefox and Chrome?

WebDriver BiDi is designed for cross-browser use, and Selenium option classes can request it. Actual module and event coverage depends on the browser, driver, Selenium version, and Grid, so verify the exact scenarios in your support matrix.

How do I remove a Python Selenium network handler?

Save the integer returned by add_request_handler and pass it with the same event name to remove_request_handler. You can also call clear_request_handlers during fixture teardown when the fixture owns all handlers for that driver.

Can WebDriver BiDi read response bodies in Python?

Do not assume a public response-body helper exists just because CDP or another binding has one. Check the current high-level Python API for the exact feature, and use a proxy or direct API test when response capture is outside that supported surface.

Related Guides