QA How-To
Selenium BiDi vs Playwright Network Interception (2026)
Compare selenium bidi vs playwright network interception with runnable examples for mocking, modifying, blocking, observing, and choosing a test stack.
21 min read | 3,059 words
TL;DR
Playwright has the simpler and more complete testing workflow for request blocking, mutation, passthrough, response replacement, and response patching. Selenium BiDi is the stronger strategic fit for teams committed to WebDriver and cross-vendor standards, but its Java BiDi surface is beta in 2026 and requires more protocol-aware setup.
Key Takeaways
- Choose Playwright when concise request routing and response mocking are central to the suite.
- Choose Selenium BiDi when WebDriver interoperability, an existing Selenium estate, or standards alignment matters more than API convenience.
- Register interception before navigation or before the user action that starts the request.
- Use context-wide Playwright routes for popups and multiple pages, and page routes for local test scope.
- Treat Selenium Java BiDi APIs as beta in 2026 and pin the Selenium version before adopting them broadly.
- Assert the visible application result as well as the intercepted request so a mock cannot create a false-positive UI test.
Selenium BiDi vs Playwright network interception is mainly a choice between a standards-oriented WebDriver capability and a test-runner-native routing API. For a new TypeScript end-to-end suite that depends heavily on API mocking, choose Playwright. For an established Selenium suite that must preserve WebDriver tooling or follow the W3C WebDriver BiDi direction, use Selenium BiDi and isolate its beta API behind a small helper layer.
Both tools can observe traffic, pause requests, fail requests, and influence browser networking. They differ in ergonomics, maturity, scope controls, response-patching workflow, and how much test infrastructure you must assemble. This guide gives you runnable examples, verification checks, and a decision framework rather than declaring one tool universally better.
TL;DR
| Decision area | Selenium BiDi | Playwright |
|---|---|---|
| Best fit | Existing WebDriver estate and standards alignment | New browser tests with frequent network mocking |
| Request observation | BiDi network events | page.on, context.on, request waits |
| Request blocking | Add intercept, then call failRequest |
route.abort() |
| Request modification | continueRequest parameters |
route.continue() or route.fallback() overrides |
| Synthetic response | provideResponse at an intercept |
route.fulfill() |
| Patch real response | Possible through response-phase BiDi commands, with more protocol detail | route.fetch() followed by route.fulfill() |
| Scope | Browsing contexts passed to the network module | Page or browser context |
| 2026 API posture | Java BiDi classes are beta | Routing is a long-established public API |
| Language shown here | Java | TypeScript |
Use Playwright if the shortest reliable route from test intent to mock behavior is your priority. Use Selenium BiDi when migration cost, WebDriver infrastructure, or protocol portability outweighs extra code. Neither replaces API contract tests, because a browser mock proves frontend behavior against an assumed contract, not provider compatibility.
1. What Network Interception Actually Means
Network observation and network interception are related but different. Observation subscribes to events and records facts such as the URL, method, status, or timing. It does not pause the browser. Interception installs a rule at a protocol phase, pauses matching traffic, and requires the handler to continue, fail, authenticate, or satisfy the request.
That distinction matters in test design. Use observation to prove that clicking Save sent PUT /api/profile, or to collect diagnostics after a failure. Use interception to force a 503 response, remove a header, replace a backend payload, or simulate an unreachable host. If you install an intercept merely to record traffic, you add a failure path: a handler exception can leave the request paused.
Selenium BiDi exposes concepts from the WebDriver BiDi network module, including beforeRequestSent, responseStarted, responseCompleted, authRequired, and fetchError. An added intercept selects a phase at which matching traffic stops. Playwright exposes ordinary events for passive listening and routes for active handling. A matched Playwright route receives a Route plus its Request; the handler must call continue, fallback, fulfill, or abort.
For deeper event-only examples, compare capturing network traffic with Selenium and capturing network traffic with Playwright. Keeping observation separate from mutation makes failures easier to classify.
2. Prerequisites and Reproducible Setup
Use Java 21, Maven 3.9 or newer, JUnit Jupiter, Selenium Java 4.46, and a current browser for the Selenium example. Selenium Manager can resolve a compatible local driver. Firefox is a useful standards-focused choice, and the example explicitly requests the BiDi WebSocket capability.
<!-- pom.xml dependencies -->
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.46.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.13.4</version>
<scope>test</scope>
</dependency>
</dependencies>
Run mvn test once to confirm dependency and browser setup. Pin the Selenium dependency rather than using a floating range. The Java BiDi classes are marked beta in 2026, so an upgrade deserves a compile check and focused network regression run.
For Playwright, use Node.js 22 LTS and initialize a TypeScript project:
npm init playwright@latest
npx playwright install chromium
The generated project supplies @playwright/test, a configuration, and test scripts. Check installed versions with npx playwright --version and commit package-lock.json. The examples use public APIs that have existed for years, but the lockfile still ties the runner to its browser revision.
The snippets below target a sample application at http://127.0.0.1:3000 with a button named Load profile and a visible element carrying data-testid=profile-name. Replace those two application details with your own stable URL and locator. The interception code itself is complete; your application remains the system under test.
3. Selenium BiDi: Observe a Completed Response
Start with passive observation because it verifies the BiDi channel without risking a paused request. Create the driver, create Network in a try-with-resources block, subscribe before navigation, then wait on a future with a bounded timeout.
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.bidi.module.Network;
import org.openqa.selenium.bidi.network.ResponseDetails;
class SeleniumBidiNetworkTest {
@Test
void observesProfileResponse() throws Exception {
FirefoxOptions options = new FirefoxOptions();
options.setCapability("webSocketUrl", true);
try (FirefoxDriver driver = new FirefoxDriver(options);
Network network = new Network(driver)) {
CompletableFuture<ResponseDetails> profile = new CompletableFuture<>();
network.onResponseCompleted(event -> {
if (event.getResponse().getUrl().contains("/api/profile")) {
profile.complete(event);
}
});
driver.get("http://127.0.0.1:3000");
ResponseDetails event = profile.get(10, TimeUnit.SECONDS);
assertEquals(200, event.getResponse().getStatus());
}
}
}
The subscription must exist before the request. CompletableFuture safely transfers the callback result to the test thread and gives the test a clear ten-second failure boundary. Filtering inside the callback is important because a modern page may generate dozens of responses.
Verify this step by running only the class and confirming the status assertion passes. If the future times out, first inspect the actual URL and confirm the application made the request after subscription. For synchronization patterns around an action rather than navigation, see waiting for an API response in Selenium.
4. Selenium BiDi: Block a Request at the Correct Phase
Active Selenium BiDi handling requires an intercept. Add it at BEFORE_REQUEST_SENT, subscribe to the corresponding event, and fail only the target request. Every other intercepted request must continue, so the safer production design applies a URL pattern in AddInterceptParameters when your binding version supports the pattern you need. This compact example uses callback filtering to make the behavior visible.
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.bidi.module.Network;
import org.openqa.selenium.bidi.network.AddInterceptParameters;
import org.openqa.selenium.bidi.network.InterceptPhase;
class SeleniumBidiFailureTest {
@Test
void showsOfflineStateWhenProfileRequestFails() {
FirefoxOptions options = new FirefoxOptions();
options.setCapability("webSocketUrl", true);
try (FirefoxDriver driver = new FirefoxDriver(options);
Network network = new Network(driver)) {
network.addIntercept(
new AddInterceptParameters(InterceptPhase.BEFORE_REQUEST_SENT));
network.onBeforeRequestSent(event -> {
String id = event.getRequest().getRequestId();
if (event.getRequest().getUrl().contains("/api/profile")) {
network.failRequest(id);
} else {
network.continueRequest(
new org.openqa.selenium.bidi.network.ContinueRequestParameters(id));
}
});
driver.get("http://127.0.0.1:3000");
driver.findElement(By.cssSelector("[data-testid='load-profile']")).click();
new WebDriverWait(driver, Duration.ofSeconds(10)).until(
d -> d.findElement(By.cssSelector("[role='alert']"))
.getText().contains("Unable to load profile"));
}
}
}
This test models transport failure, not an HTTP 500. The application receives no response, so it should enter its network-error path. Use provideResponse when you specifically need a status and body. Because the BiDi Java API is beta, confirm the ContinueRequestParameters constructor in the pinned version during upgrades.
Verify the test by temporarily removing failRequest. The alert assertion should then fail if the real API succeeds. Restore the handler and confirm the application exposes a useful recovery state. For a focused implementation guide, continue with Selenium BiDi network interception in Java.
5. Playwright: Fulfill an API Request with JSON
Playwright turns the common mocking case into one route and one UI assertion. Register the route before navigation or before the action that makes the call. Pass a JavaScript value through json, which serializes the payload and sets an appropriate content type.
import { test, expect } from '@playwright/test';
test('renders a mocked profile', async ({ page }) => {
await page.route('**/api/profile', async route => {
await route.fulfill({
status: 200,
json: { id: 42, name: 'Ada Tester', plan: 'pro' },
});
});
await page.goto('http://127.0.0.1:3000');
await page.getByTestId('load-profile').click();
await expect(page.getByTestId('profile-name')).toHaveText('Ada Tester');
});
The glob is matched against the complete URL. **/api/profile is deliberately narrow and will not match /api/profile/42. Match the exact endpoint shape your application calls, or use a predicate when query parameters carry meaningful test data.
Verify the route actually handled the request by keeping the synthetic name unique and asserting it in the UI. A test that only checks route.fulfill() completed says nothing about frontend rendering. If you want reusable payload factories and broader Python coverage, read Playwright Python network mocking. For TypeScript-specific fulfillment options, see Playwright route.fulfill examples.
6. Playwright: Modify, Pass Through, and Patch Responses
Request mutation uses route.continue when this handler should send the request immediately to the network. The override can change headers, method, post data, or a same-protocol URL. Build headers from the request so required browser headers remain intact.
import { test, expect } from '@playwright/test';
test('sends an experiment header', async ({ page }) => {
await page.route('**/api/recommendations', async route => {
await route.continue({
headers: {
...route.request().headers(),
'x-test-cohort': 'network-guide',
},
});
});
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/recommendations') && response.status() === 200
);
await page.goto('http://127.0.0.1:3000');
await responsePromise;
await expect(page.getByTestId('recommendations')).toBeVisible();
});
continue ends route dispatch, so later matching handlers do not run. Use route.fallback() instead when layered handlers should get a chance to process the request. This difference prevents subtle bugs in a shared routing fixture.
To preserve a real response and alter one field, fetch it through the route and fulfill with the original metadata plus an overridden body:
await page.route('**/api/profile', async route => {
const upstream = await route.fetch();
const profile = await upstream.json();
await route.fulfill({
response: upstream,
json: { ...profile, plan: 'enterprise-test' },
});
});
Verify response patching with a visible plan badge and, when important, assert upstream.ok() before parsing. This pattern depends on the upstream service, so it is not a fully offline mock. It is valuable for adding one edge field while retaining realistic headers and payload shape, but a provider outage can still fail the test.
7. Request Failure, HTTP Errors, and Timing
A failed request and an HTTP error exercise different application branches. In Playwright, route.abort('connectionrefused') simulates a transport-level failure. route.fulfill({ status: 503, json: ... }) delivers a valid HTTP response whose status indicates failure. In Selenium BiDi, failRequest models transport failure, while provideResponse supplies a synthetic response at an installed intercept.
Choose the failure according to frontend code. A fetch promise rejects for a network failure, but normally resolves for HTTP 503 and requires application code to inspect response.ok or the status. Testing only one path leaves the other error handler unproved.
test('distinguishes service errors from lost connectivity', async ({ page }) => {
await page.route('**/api/profile', route => route.fulfill({
status: 503,
headers: { 'retry-after': '30' },
json: { code: 'PROFILE_TEMPORARILY_UNAVAILABLE' },
}));
await page.goto('http://127.0.0.1:3000');
await page.getByTestId('load-profile').click();
await expect(page.getByRole('alert')).toContainText('Try again');
});
Do not use a fixed sleep to make the failure feel slow. Delay belongs in a narrowly scoped test helper only when latency behavior is the requirement, and the assertion should wait on an observable state such as a loading indicator. Ordinary UI tests should fulfill promptly and rely on retrying assertions.
When you need to coordinate an actual response with a click, create the wait first, perform the action, and await the saved promise. The Playwright waitForResponse examples show this ordering without interception.
8. Scope, Popups, Service Workers, and Handler Cleanup
Scope often decides whether a locally correct mock survives a complete suite. page.route() affects one Playwright page. browserContext.route() covers pages in that context, including popups and new tabs. Prefer page scope for a single-page scenario and context scope when the tested journey deliberately crosses pages. Do not place mutable, test-specific routes in a shared context.
Remove a Playwright rule with page.unroute() or browserContext.unroute() if behavior must change during one test. Normally, fresh test contexts provide automatic isolation. If multiple routes match, understand registration order and use fallback for chaining. A broad **/* route is expensive conceptually even when execution remains fast because every asset becomes part of the handler's correctness.
Selenium's Network can be created for the driver or selected browsing context IDs. Keep it in try-with-resources so subscriptions are closed. Save the intercept ID returned by addIntercept when you need explicit removal. Avoid static listeners and shared futures because callbacks from one test can complete another test's state.
Service workers add a special caveat. They can satisfy requests before page routing sees them, depending on browser behavior and application architecture. For Playwright tests where interception must own all relevant traffic, configure the browser context to block service workers. For Selenium, test support against the specific browser and driver combination rather than assuming every request will appear at the same phase. WebSocket traffic also has separate APIs and semantics from ordinary HTTP request routing.
Verify scope by opening the popup or second tab that matters and asserting the mock value there. A route that passes only on the original page is not sufficient evidence for a multi-page workflow.
9. Portability, Maturity, and Maintenance Cost
WebDriver BiDi is designed as a bidirectional browser automation protocol rather than a Chromium-only debugging attachment. That direction is valuable for Selenium teams seeking cross-browser event and command semantics without binding their framework to Chrome DevTools Protocol versions. It does not mean every browser, binding, and network command has identical maturity today. Capability support must be part of your test matrix.
Selenium 4.46 marks the current Java BiDi classes beta. Beta is not the same as unusable. It means you should expect source-level adjustments and protect tests from churn. Put network construction, intercept registration, and response helpers behind a small project-owned interface. Compile that adapter against proposed upgrades before changing the main suite.
Playwright owns its runner, browser integrations, contexts, events, and routing abstraction. That vertical integration produces a compact developer experience. The trade-off is that Playwright is its own automation stack, not a drop-in network add-on for a Selenium framework. Migrating solely for route.fulfill() may be unjustified when thousands of stable WebDriver tests, Grid infrastructure, reporting hooks, and team expertise already exist.
Do not equate fewer lines with universally lower cost. Count retraining, CI images, browser policies, test data, tracing, parallel execution, and failure triage. A ten-line Playwright mock is attractive, but a second framework can create more organizational complexity than it removes. Conversely, building a thick abstraction around evolving Selenium BiDi calls for every new frontend project may cost more than standardizing on Playwright.
10. Selenium BiDi vs Playwright Network Interception by Scenario
Use the scenario, not brand preference, to make the call.
| Scenario | Better default | Why |
|---|---|---|
| New TypeScript UI suite with many mocked APIs | Playwright | Direct routing, JSON fulfillment, fixtures, and web-first assertions live in one stack |
| Large Java Selenium suite adding two failure cases | Selenium BiDi | Avoids a second runner and reuses existing page objects, Grid, and reporting |
| Cross-vendor protocol research | Selenium BiDi | Tracks the W3C WebDriver BiDi model |
| Patch a real JSON response in one test | Playwright | route.fetch() plus route.fulfill() is concise |
| Popup must inherit the same mock | Playwright context route | Context scope naturally covers created pages |
| Authentication challenge handling in WebDriver | Selenium BiDi | The network module exposes the auth-required phase and credential commands |
| Backend compatibility assurance | Neither alone | Use contract or integration tests against real provider behavior |
| Browser-specific low-level experiment | Evaluate directly | Support may depend on browser, binding, and protocol version |
A mixed organization can use both without duplicating every scenario. Assign tool ownership by product or test layer. For example, retain Selenium for mature cross-browser release journeys and use Playwright for a new frontend's dense mocked-state tests. Share contract fixtures at the data level, not page-object abstractions across incompatible runners.
Whatever you choose, maintain one canonical schema for mock payloads. Validate fixtures against the provider contract in CI. Consumer-driven API contract testing with Pact addresses a different risk from browser interception and prevents mocks from silently drifting away from the backend.
Which Should You Choose
Choose Playwright for most greenfield network-heavy browser testing. Its page and context routes express scope clearly, fulfill handles static responses, fetch supports selective response patching, abort models transport failures, and fallback enables deliberate handler composition. The runner also gives you isolated browser contexts and retrying UI assertions without assembling separate libraries.
Choose Selenium BiDi when the suite is already built around WebDriver, Java or another Selenium binding is a firm platform choice, Grid investment is material, or standards-based browser automation is a strategic requirement. Start with passive event capture, add a small number of high-value intercept cases, and hide beta API details behind a thin adapter. Test browser support in CI before promising identical behavior across the matrix.
Do not migrate a healthy suite for syntax alone. Build one representative spike: mock a success payload, simulate a transport failure, run it on required browsers, execute it in CI, and inspect an intentional failure. Compare implementation time, reliability, diagnostic output, and upgrade exposure. That evidence is more useful than a generic feature checklist.
Use the product surfaces when the real goal is career preparation rather than framework migration. You can analyze your resume against a target role in Resume Studio and rehearse concise explanations of interception trade-offs in QA interview practice.
Interview Questions and Answers
Q: What is the main difference between Selenium BiDi and Playwright routing?
Selenium BiDi exposes network commands and events aligned with the WebDriver BiDi protocol, while Playwright exposes a runner-native route abstraction. Playwright is usually more concise for mocks. Selenium BiDi fits teams that need WebDriver continuity or standards alignment.
Q: Why must an interception handler always resolve the request?
The browser pauses matching traffic at the chosen phase. If the handler neither continues, fulfills, nor fails it, the page can hang until a timeout. Narrow matching and defensive error handling reduce that risk.
Q: How do you test HTTP 500 separately from network loss?
Fulfill a synthetic response with status 500 for the HTTP case. Abort or fail the request for transport loss. Assert different user-facing behavior when the application intentionally distinguishes them.
Q: When should you use a context route in Playwright?
Use it when multiple pages or popups in the same isolated context need the rule. Prefer a page route when the mock belongs to one page because narrower scope reduces accidental matches.
Q: Does interception replace contract testing?
No. The mock proves how the frontend behaves against test-controlled data. A contract test proves whether consumer and provider expectations remain compatible.
Q: How would you control Selenium BiDi upgrade risk?
Pin Selenium, wrap beta BiDi details in a small adapter, and compile and run targeted network tests before an upgrade. Keep the adapter thin so protocol changes do not spread through page objects.
Common Mistakes
- Registering a route after navigation has already started the target request. Install it before the triggering action.
- Using
abortto represent HTTP 503. Abort creates transport failure; fulfill a 503 response to exercise status handling. - Matching
**/*and forgetting to continue unrelated assets. Prefer endpoint-specific patterns. - Asserting only that a callback ran. Verify the visible UI state produced by the response.
- Calling Playwright
route.continue()when another registered handler must run. Useroute.fallback()for handler chaining. - Sharing route state or Selenium futures across parallel tests. Create state within each isolated scenario.
- Assuming response mocks validate the real backend. Add schema, contract, or integration coverage.
- Leaving Selenium intercepts and subscriptions open. Close
Networkand remove temporary intercepts when scope changes. - Ignoring service workers, redirects, and popups. Prove interception at the browser boundary your application actually uses.
- Copying CDP examples into a BiDi design. CDP and WebDriver BiDi have different protocols, support boundaries, and lifecycle APIs.
- Upgrading Selenium without compiling beta BiDi calls. Pin versions and run a focused compatibility check first.
- Adding fixed sleeps after fulfillment. Wait for a user-visible state or a precisely identified response.
Conclusion
For selenium bidi vs playwright network interception in 2026, Playwright is the practical default for a new suite centered on request and response manipulation. Its routing API covers common testing jobs with less ceremony and clear page or context scope. Selenium BiDi is the sound choice when preserving WebDriver investments and following the emerging cross-browser protocol matter more than having the shortest mocking syntax.
Implement one high-value failure path first. Register the handler before the request, distinguish HTTP errors from transport failures, assert what the user sees, and validate mock payloads outside the browser test. That workflow produces trustworthy coverage regardless of which automation stack owns the network boundary.
Interview Questions and Answers
How would you compare Selenium BiDi with Playwright network interception?
Selenium BiDi exposes standards-oriented network phases, events, and commands within WebDriver. Playwright offers a mature, test-runner-native routing API with concise fulfillment, abort, passthrough, and response-patching workflows. I choose based on the existing stack, required browsers, mocking density, and tolerance for beta API changes.
Why register a network handler before the user action?
The request can begin immediately after navigation, rendering, or a click. Registering first removes the race in which the browser sends traffic before the test is listening. For event waits, I create the promise first, perform the action second, and await the saved promise last.
How do you prevent a network mock from creating a false positive?
I use a unique fixture value and assert its visible effect in the UI. I also validate the fixture against a schema or consumer contract and retain integrated coverage against the real service. An intercepted callback alone is not proof that the application handled the payload correctly.
When would you use route.fetch in Playwright?
I use route.fetch when I want the real upstream response but need to alter a small part before the page receives it. I parse the response, change the required field, and fulfill using the upstream response plus the overridden body. I avoid it for fully deterministic offline mocks because the upstream dependency remains live.
How do you model network failure versus server failure?
For network failure, I abort the Playwright route or call Selenium BiDi failRequest. For server failure, I provide a real HTTP status such as 503 with a representative error body and headers. The distinction matters because fetch rejects transport errors but resolves HTTP error responses.
What risks come with broad interception patterns?
A broad pattern pauses unrelated scripts, images, analytics, and API calls. One forgotten continuation can hang navigation, while one accidental fulfillment can hide a real defect. I match the smallest stable endpoint pattern and explicitly handle every matched request path.
Frequently Asked Questions
Is Selenium BiDi network interception stable in 2026?
The WebDriver BiDi direction is standards-based, but Selenium 4.46 marks the current Java BiDi classes as beta. Pin the Selenium version, wrap network calls in a thin adapter, and run compatibility tests before upgrades.
Is Playwright better than Selenium for network mocking?
Playwright is usually easier for network-heavy tests because route, fulfill, fetch, continue, fallback, and abort form one cohesive API. Selenium can still be the better overall choice when an established WebDriver suite and Grid investment outweigh API ergonomics.
Can Selenium BiDi mock an API response?
Yes. Add an intercept at the appropriate phase and use the network module's provideResponse command with a request ID and synthetic response parameters. Exact parameter types are binding-specific, so pin and verify the Selenium version.
What is the difference between Playwright route continue and fallback?
Continue sends the request to the network immediately and prevents later matching route handlers from running. Fallback passes control to the next matching handler and can carry request overrides.
Should I mock every API in browser tests?
No. Mock targeted states that are difficult, destructive, or slow to create, while retaining a small set of integrated journeys against real services. Validate shared fixtures against API schemas or contracts so browser mocks do not drift.
Does a failed request equal an HTTP 500 response?
No. A failed request represents a transport problem and typically rejects fetch. An HTTP 500 is a completed response that application code must classify by status, so test the two branches separately.