Network Interception with BiDi
Observe, block, modify and mock HTTP traffic across browsers: capture requests and responses, fail third-party calls, inject headers, stub API responses, and build a network-idle wait.
Network control is what separates end-to-end tests that fight their environment from tests that own it. With BiDi’s network module you can watch every request the page makes, block the analytics beacons that slow it down, add an auth header to every API call, or return a canned response so the UI shows the “no results” state without touching the backend. Everything here works in Chrome, Edge and Firefox with the same code.
The Model
The network module emits events at each phase of a request and lets you intercept at three of them:
| Event | Interceptable phase | What you can do |
|---|---|---|
beforeRequestSent | beforeRequestSent | Continue (optionally with new URL, method, headers, body), fail, or provide a response outright |
responseStarted | responseStarted | Continue with modified status or headers |
authRequired | authRequired | Provide credentials, continue with default handling, or cancel |
responseCompleted | (observation only) | Inspect status, headers, timings |
fetchError | (observation only) | Inspect failure reason |
Intercepts are registered with URL patterns (glob-like **/api/** or exact strings). A request that matches a pattern at an intercepted phase is paused until your handler continues it, so always call a continuation in every branch, or the page hangs.
Observing Traffic
Start with observation. It is the lowest-risk use and immediately useful for debugging and assertions.
import org.openqa.selenium.bidi.module.Network;
// options.setCapability("webSocketUrl", true) when creating the driverNetwork network = new Network(driver);List<String> apiCalls = new CopyOnWriteArrayList<>();
network.onBeforeRequestSent(event -> { String url = event.getRequest().getUrl(); if (url.contains("/api/")) apiCalls.add(event.getRequest().getMethod() + " " + url);});
network.onResponseCompleted(event -> { if (event.getResponseData().getStatus() >= 500) { System.err.println("Server error: " + event.getResponseData().getUrl()); }});
driver.get("https://app.example.com/orders");driver.findElement(By.id("refresh")).click();
new WebDriverWait(driver, Duration.ofSeconds(10)) .until(d -> apiCalls.stream().anyMatch(c -> c.equals("GET https://app.example.com/api/orders?page=1")));api_calls = []
def on_request(request): if "/api/" in request.url: api_calls.append(f"{request.method} {request.url}")
def on_response(response): if response.status >= 500: print("Server error:", response.url)
driver.network.add_request_handler(on_request) # observes; does not pause requestsdriver.network.add_response_handler(on_response)
driver.get("https://app.example.com/orders")driver.find_element(By.ID, "refresh").click()
WebDriverWait(driver, 10).until( lambda d: "GET https://app.example.com/api/orders?page=1" in api_calls)const { Network } = require('selenium-webdriver/bidi/network');
const network = await Network(driver);const apiCalls = [];
await network.beforeRequestSent((event) => {const { url, method } = event.request;if (url.includes('/api/')) apiCalls.push(`${method} ${url}`);});
await network.responseCompleted((event) => {if (event.response.status >= 500) console.error('Server error:', event.response.url);});
await driver.get('https://app.example.com/orders');await driver.findElement(By.id('refresh')).click();
await driver.wait(() => apiCalls.includes('GET https://app.example.com/api/orders?page=1'), 10000);var bidi = await driver.AsBiDiAsync();var apiCalls = new ConcurrentBag<string>();
await bidi.Network.OnBeforeRequestSentAsync(e =>{ if (e.Request.Url.Contains("/api/")) apiCalls.Add($"{e.Request.Method} {e.Request.Url}");});
await bidi.Network.OnResponseCompletedAsync(e =>{ if (e.Response.Status >= 500) Console.Error.WriteLine($"Server error: {e.Response.Url}");});
driver.Navigate().GoToUrl("https://app.example.com/orders");driver.FindElement(By.Id("refresh")).Click();
new WebDriverWait(driver, TimeSpan.FromSeconds(10)) .Until(_ => apiCalls.Contains("GET https://app.example.com/api/orders?page=1"));Blocking Requests
Block third-party scripts, analytics and ads. Tests get faster and stop depending on hosts you do not control.
import org.openqa.selenium.bidi.network.AddInterceptParameters;import org.openqa.selenium.bidi.network.InterceptPhase;import org.openqa.selenium.bidi.network.UrlPattern;
Network network = new Network(driver);
String intercept = network.addIntercept( new AddInterceptParameters(InterceptPhase.BEFORE_REQUEST_SENT) .urlPatterns(List.of( new UrlPattern().hostname("*.googletagmanager.com"), new UrlPattern().hostname("*.doubleclick.net"), new UrlPattern().pathname("/analytics/*"))));
network.onBeforeRequestSent(event -> { if (event.isBlocked()) { network.failRequest(event.getRequest().getRequestId()); }});
driver.get("https://app.example.com");// ... laternetwork.removeIntercept(intercept);from selenium.webdriver.common.bidi.network import Request
def block(request: Request): request.fail()
driver.network.add_request_handler( ["**googletagmanager.com/**", "**doubleclick.net/**", "**/analytics/**"], block)
driver.get("https://app.example.com")# ... laterdriver.network.clear_request_handlers()const { AddInterceptParameters } = require('selenium-webdriver/bidi/addInterceptParameters');const { InterceptPhase } = require('selenium-webdriver/bidi/interceptPhase');
const network = await Network(driver);const intercept = await network.addIntercept(new AddInterceptParameters(InterceptPhase.BEFORE_REQUEST_SENT) .urlStringPattern('https://www.googletagmanager.com/*'));
await network.beforeRequestSent(async (event) => {if (event.isBlocked) await network.failRequest(event.request.request);});
await driver.get('https://app.example.com');await network.removeIntercept(intercept);var bidi = await driver.AsBiDiAsync();
await using var intercept = await bidi.Network.InterceptRequestAsync(async e =>{ var host = new Uri(e.Request.Url).Host; if (host.EndsWith("googletagmanager.com") || host.EndsWith("doubleclick.net")) await e.Request.FailAsync(); else await e.Request.ContinueAsync();}, new() { UrlPatterns = [new UrlPatternPattern { Hostname = "*" }] });
driver.Navigate().GoToUrl("https://app.example.com");A blocked request appears to the page as a network error, the same as if the host were down. Make sure the app tolerates that for the hosts you block.
Modifying Requests: Headers and URLs
Add an authorization header to every API call, point the app at a different backend, or tag requests so the server can identify test traffic.
import org.openqa.selenium.bidi.network.ContinueRequestParameters;import org.openqa.selenium.bidi.network.Header;import org.openqa.selenium.bidi.network.BytesValue;
Network network = new Network(driver);network.addIntercept(new AddInterceptParameters(InterceptPhase.BEFORE_REQUEST_SENT) .urlPatterns(List.of(new UrlPattern().pathname("/api/*"))));
network.onBeforeRequestSent(event -> { if (!event.isBlocked()) return; String id = event.getRequest().getRequestId(); ContinueRequestParameters params = new ContinueRequestParameters(id) .headers(List.of( new Header("Authorization", new BytesValue(BytesValue.Type.STRING, "Bearer test-token")), new Header("X-Test-Run", new BytesValue(BytesValue.Type.STRING, "ci-4812"))));
// Optionally redirect to a stub backend String url = event.getRequest().getUrl(); if (url.startsWith("https://api.example.com/")) { params.url(url.replace("https://api.example.com/", "https://stub.internal/")); } network.continueRequest(params);});def add_headers(request): headers = {"Authorization": "Bearer test-token", "X-Test-Run": "ci-4812"} url = request.url if url.startswith("https://api.example.com/"): url = url.replace("https://api.example.com/", "https://stub.internal/") request.continue_request(url=url, headers=headers)
driver.network.add_request_handler(["**/api/**"], add_headers)
# Simpler when you only need constant headers on everything:driver.network.set_extra_headers({"X-Test-Run": "ci-4812"})const { ContinueRequestParameters } = require('selenium-webdriver/bidi/continueRequestParameters');const { Header, BytesValue } = require('selenium-webdriver/bidi/networkTypes');
const network = await Network(driver);await network.addIntercept(new AddInterceptParameters(InterceptPhase.BEFORE_REQUEST_SENT).urlStringPattern('https://api.example.com/*'));
await network.beforeRequestSent(async (event) => {if (!event.isBlocked) return;const params = new ContinueRequestParameters(event.request.request).headers([ new Header('Authorization', new BytesValue(BytesValue.Type.STRING, 'Bearer test-token')), new Header('X-Test-Run', new BytesValue(BytesValue.Type.STRING, 'ci-4812')),]);await network.continueRequest(params);});var bidi = await driver.AsBiDiAsync();
await bidi.Network.InterceptRequestAsync(async e =>{ await e.Request.ContinueAsync(new() { Headers = [ new Header("Authorization", "Bearer test-token"), new Header("X-Test-Run", "ci-4812"), ] });}, new() { UrlPatterns = [new UrlPatternPattern { Pathname = "/api/*" }] });Mocking Responses
Return a fake response without the request ever leaving the browser. This is how you test empty states, error states and slow states deterministically.
import org.openqa.selenium.bidi.network.ProvideResponseParameters;
Network network = new Network(driver);network.addIntercept(new AddInterceptParameters(InterceptPhase.BEFORE_REQUEST_SENT) .urlPatterns(List.of(new UrlPattern().pathname("/api/orders"))));
network.onBeforeRequestSent(event -> { if (!event.isBlocked()) return; network.provideResponse(new ProvideResponseParameters(event.getRequest().getRequestId()) .statusCode(200) .headers(List.of(new Header("Content-Type", new BytesValue(BytesValue.Type.STRING, "application/json")))) .body(new BytesValue(BytesValue.Type.STRING, "{\"orders\": [], \"total\": 0}")));});
driver.get("https://app.example.com/orders");assertTrue(driver.findElement(By.cssSelector("[data-testid='empty-state']")).isDisplayed());
// Error state: swap the status// .statusCode(503).body(new BytesValue(BytesValue.Type.STRING, "{\"error\":\"maintenance\"}"))def stub_orders(request): request.provide_response( status_code=200, headers={"Content-Type": "application/json"}, body='{"orders": [], "total": 0}', )
driver.network.add_request_handler(["**/api/orders"], stub_orders)
driver.get("https://app.example.com/orders")assert driver.find_element(By.CSS_SELECTOR, "[data-testid='empty-state']").is_displayed()
# Error state: status_code=503, body='{"error": "maintenance"}'const { ProvideResponseParameters } = require('selenium-webdriver/bidi/provideResponseParameters');
const network = await Network(driver);await network.addIntercept(new AddInterceptParameters(InterceptPhase.BEFORE_REQUEST_SENT).urlStringPattern('https://app.example.com/api/orders'));
await network.beforeRequestSent(async (event) => {if (!event.isBlocked) return;await network.provideResponse( new ProvideResponseParameters(event.request.request) .statusCode(200) .headers([new Header('Content-Type', new BytesValue(BytesValue.Type.STRING, 'application/json'))]) .body(new BytesValue(BytesValue.Type.STRING, '{"orders": [], "total": 0}')));});
await driver.get('https://app.example.com/orders');var bidi = await driver.AsBiDiAsync();
await bidi.Network.InterceptRequestAsync(async e =>{ await e.Request.ProvideResponseAsync(new() { StatusCode = 200, Headers = [new Header("Content-Type", "application/json")], Body = "{\"orders\": [], \"total\": 0}", });}, new() { UrlPatterns = [new UrlPatternPattern { Pathname = "/api/orders" }] });
driver.Navigate().GoToUrl("https://app.example.com/orders");Assert.That(driver.FindElement(By.CssSelector("[data-testid='empty-state']")).Displayed);Two rules for mocks: record real responses first (observe mode) so your stubs match the contract, and keep a test that hits the real backend so drift is caught.
A Network-Idle Wait
Count in-flight requests and wait for zero plus a quiet period. Filter out long-polling and websocket keep-alives or it never settles. Full code in Custom Wait Conditions.
Debugging Interceptors
- Page hangs after adding an intercept: a handler branch did not continue, fail or provide. Every intercepted request must be resolved.
- Handler never fires: the pattern does not match. Patterns compare against the full URL; hostname patterns need wildcards for subdomains, and pathname patterns are anchored.
- Works in Chrome, not Firefox: check that the phase you intercept is implemented in that Firefox version;
responseStartedmodification landed later thanbeforeRequestSent. - Events arrive after the test ended: the session outlived the test. Remove intercepts and handlers in teardown.
Summary
- BiDi’s network module observes and intercepts traffic at
beforeRequestSent,responseStartedandauthRequired, across browsers. - Observe to assert on API calls; block to remove third-party noise; modify to inject headers; provide to mock states.
- Always resolve intercepted requests, and scope patterns tightly.
- Record real traffic before writing mocks, and keep one unmocked test per endpoint.
Copy-paste recipes for this topic
- Block Analytics and Third-Party Requests
Stop trackers, chat widgets and ad scripts from loading during tests to speed up pages and remove external flakiness.
- Pass HTTP Basic Authentication
Get through a browser's basic auth prompt with BiDi's authentication handler, and the URL-embedded fallback for older setups.
- Mock an API Response
Return a canned JSON payload for one endpoint so the UI shows empty, error or edge-case states without changing the backend.