PRACTICAL GUIDE / Selenium BiDi request header modification
Modify Request Headers with Selenium WebDriver BiDi
Learn Selenium BiDi request header modification with working code, failure cases, debugging steps, and CI evidence for reliable QA automation.
In this guide11 sections
- Define What the Header Is Allowed to Change
- Choose Between Interception and Simpler Controls
- Verify the Java API and Correct Phase
- Rebuild the Header List Deliberately
- Execute a Request-Scoped Java Workflow
- Correlate the Mutation with Server and Product Evidence
- Use Negative Controls for Scope and Secrecy
- Debug Missing, Duplicate, or Stalled Continuations
- Own Cleanup and Parallel CI Behavior
- Frequently Asked Questions
- Does continueRequest add one header or replace all request headers?
- How can a test prove the modified header reached the server?
- Which phase is valid for Selenium BiDi request header modification?
- Can I inject Authorization or Cookie with a generic header interceptor?
- Why does a request hang after I modify its headers?
- How should header-modification tests behave in CI?
- Practice a Header Contract in QABattle
What you will learn
- Define What the Header Is Allowed to Change
- Choose Between Interception and Simpler Controls
- Verify the Java API and Correct Phase
- Rebuild the Header List Deliberately
Selenium BiDi request header modification should pause one outgoing request, replace its header list without losing required fields, continue it by request ID, and verify the value at a controlled server boundary. Subscribe before the browser action, keep credentials out of generic mutation, assert an unmodified control request, and remove every intercept the test creates.
Define What the Header Is Allowed to Change
A header mutation test needs a narrow behavioral purpose. Good examples include selecting a test-only feature variant, adding a non-secret trace marker, exercising locale negotiation, or validating an application gateway contract. The assertion should describe the server or product outcome: the endpoint selected variant bidi-canary, the response returned the expected locale, or the trace appeared under the current case ID. "The callback built a header" proves only local Java code.
Draw the boundary before choosing a field. The browser creates the original request, the BiDi intercept pauses it, the Java callback constructs a replacement list, the browser sends the resumed request, the server consumes it, and the application renders a result. Each boundary can fail independently. The classic-command and BiDi-event correlation guide is useful here because a successful continueRequest command does not prove server receipt or a correct UI state.
Use a harmless, environment-specific field such as X-QA-Variant in examples and internal fixtures. Avoid Host, Content-Length, connection-specific fields, and browser-generated security metadata. Never use a broad callback to add Authorization or Cookie to every request. The Selenium BiDi authentication guide covers HTTP challenge handling, where credentials have a dedicated phase and continuation.
The event and intercept lifecycle is explicit: create the module, add the intercept, register listeners, trigger the browser request, receive a blocked beforeRequestSent, continue that request once, observe a response or fetch error with the same request ID, assert server-backed product state, remove the intercept, close listeners, then quit the driver. The first observable assertion is a blocked event tied to the expected intercept. The first delivery assertion belongs at the server echo or resulting application state.
Choose Between Interception and Simpler Controls
BiDi is valuable when the browser request itself is the subject. It is unnecessary when the application already accepts a stable test configuration, when an API client can establish state before the UI flow, or when the test only needs observation. Choose the least invasive mechanism that still crosses the risk boundary.
| Requirement | Preferred control | What it proves | Important limitation |
|---|---|---|---|
| Confirm a header the browser already sends | Observe beforeRequestSent | Browser-side request metadata | Does not prove server receipt |
| Change one request after the page initiates it | Scoped intercept plus continueRequest | Browser behavior under a modified request | Requires total callback handling |
| Apply one field to an entire controlled context | Binding-supported extra-header API, if available | Context policy | Wider scope can affect redirects and subresources |
| Establish authentication state | Auth flow, cookie API, or approved fixture | Intended credential mechanism | Generic headers can leak on redirects |
| Select server behavior for many tests | Test environment configuration | Stable integration behavior | Does not test browser-side mutation |
An intercept is appropriate when request timing or browser ownership matters. For example, a single fetch created after a user click must carry a variant marker, while earlier boot requests must remain untouched. A global proxy or process-wide setting would obscure that boundary. The intercept should match scheme, host, path, and top-level browsing context, then the callback should verify the actual event again.
When migrating an existing Chrome DevTools Protocol helper, inventory what it really did. Some CDP helpers append fields globally, while continueRequest with headers replaces the list for one blocked request. The Selenium CDP to BiDi migration guide helps separate protocol syntax from the old test's purpose. Preserve the server assertion, negative control, and cleanup policy, not just the method call.
Do not combine several mutation mechanisms in one session unless the order is the requirement under test. Browser profile headers, proxy injection, service workers, and BiDi continuation can each alter the outgoing request. A passing server echo then cannot identify which owner supplied the value. Keep one controller active and make its scope visible in the case evidence.
Verify the Java API and Correct Phase
The code in this article targets Selenium Java 4.44.0, verified from the tagged binding source on July 18, 2026. That version exposes Network.continueRequest(ContinueRequestParameters), and ContinueRequestParameters.headers(List<Header>) accepts Selenium BiDi Header values. These classes can change as implementations converge, so pin the client version and compile examples against the same dependency used in CI.
Enable BiDi with webSocketUrl before creating the driver. The official Selenium BiDi overview describes this capability and the asynchronous WebSocket channel. A classic session that navigates successfully is not proof that a remote Grid or provider has exposed the BiDi path. Run a small startup probe that creates Network and receives one event before executing mutation cases.
Request headers are changed at BEFORE_REQUEST_SENT. The official Selenium W3C network page shows intercept registration and network event access. Under the W3C network module, continueRequest is valid only while the request is blocked at beforeRequestSent. continueResponse belongs to responseStarted, and auth commands belong to authRequired.
That phase rule is a useful diagnostic boundary. If event.isBlocked() is false, do not send a continuation. If the blocked event does not list the intercept ID owned by the test, another owner may have paused it. If the request ID has already been resumed, a second command should fail rather than being treated as harmless. Model the handler as a one-shot decision for each owned request ID.
Selenium's network features documentation describes request handlers at a higher level, but examples can lag a released binding. Check the installed artifact or tagged source for every method used by a shared framework. Avoid reflection-based compatibility layers that convert a missing API into a runtime surprise; a small version-specific adapter with compile-time tests is easier to own.
Rebuild the Header List Deliberately
The central semantic detail is replacement. The W3C steps create a new header list from the values supplied to continueRequest and set the request's header list to it. Therefore, passing only X-QA-Variant can discard original fields needed for cookies, content type, accepted encodings, tracing, origin checks, or conditional requests. Start from event.getRequest().getHeaders() and preserve everything except the field intentionally replaced.
Header names are case-insensitive. Remove all existing occurrences using equalsIgnoreCase, then add one new value unless the field is legally multi-valued and the product contract requires another policy. Do not rely on map conversion without deciding how duplicate fields should behave. A Map<String, String> can silently collapse valid repeated values such as Accept variants and loses original ordering that may help diagnosis.
Java example 1: construct a case-insensitive replacement list
import java.util.ArrayList;
import java.util.List;
import org.openqa.selenium.bidi.network.BytesValue;
import org.openqa.selenium.bidi.network.Header;
final class RequestHeaders {
private RequestHeaders() {}
static List<Header> replace(List<Header> original, String name, String value) {
List<Header> result = new ArrayList<>();
for (Header header : original) {
if (!header.getName().equalsIgnoreCase(name)) {
result.add(header);
}
}
result.add(new Header(name, new BytesValue(BytesValue.Type.STRING, value)));
return List.copyOf(result);
}
static long count(List<Header> headers, String name) {
return headers.stream()
.filter(header -> header.getName().equalsIgnoreCase(name))
.count();
}
}Unit test this pure function before putting it on an event thread. Cover an absent field, one existing value, duplicate values with mixed casing, an empty original list, and preservation of unrelated repeated fields. Assert that input lists are not mutated. These tests are fast enough for every change and isolate collection mistakes from browser or network behavior.
Values can be represented as string or base64 bytes through BytesValue. Use string for a printable test marker. Validate length and characters before registration, and never print a sensitive value in an assertion diff. If the target may contain arbitrary bytes, define encoding at the server contract rather than improvising in the callback.
Execute a Request-Scoped Java Workflow
Use this ordered workflow for implementation and review:
- Generate a unique, non-secret case marker and configure a controlled echo endpoint.
- Start a BiDi-enabled browser, open the fixture, and create a
Networkscoped to its top-level context. - Add an exact
BEFORE_REQUEST_SENTintercept and retain the returned intercept ID. - Register response, fetch-error, and blocked-request listeners before clicking the trigger.
- For the owned blocked event, copy original headers, replace one allowlisted field, and call
continueRequestwith that request ID. - Wait for a response carrying the same request ID and for the fixture to display the server-observed marker.
- Send a control request outside the pattern and assert that the server did not observe the injected field.
- Remove the intercept in
finally, close the network module, and quit the driver after cleanup.
Java example 2: modify one request and verify a server-backed result
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.bidi.module.Network;
import org.openqa.selenium.bidi.network.AddInterceptParameters;
import org.openqa.selenium.bidi.network.ContinueRequestParameters;
import org.openqa.selenium.bidi.network.Header;
import org.openqa.selenium.bidi.network.InterceptPhase;
import org.openqa.selenium.bidi.network.UrlPattern;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.WebDriverWait;
class FeatureHeaderTest {
@Test
void sendsVariantHeaderOnlyToFeatureEndpoint() throws Exception {
String marker = "bidi-canary-42";
ChromeOptions options = new ChromeOptions();
options.setCapability("webSocketUrl", true);
WebDriver driver = new ChromeDriver(options);
try {
driver.get("https://app.example.test/header-lab");
AtomicReference<String> requestId = new AtomicReference<>();
CompletableFuture<String> completedId = new CompletableFuture<>();
try (Network network = new Network(driver.getWindowHandle(), driver)) {
String interceptId = network.addIntercept(
new AddInterceptParameters(InterceptPhase.BEFORE_REQUEST_SENT)
.urlPattern(new UrlPattern()
.protocol("https")
.hostname("api.example.test")
.pathname("/feature-flags")));
try {
network.onBeforeRequestSent(event -> {
if (!event.isBlocked() || !event.getIntercepts().contains(interceptId)) {
return;
}
String id = event.getRequest().getRequestId();
URI uri = URI.create(event.getRequest().getUrl());
if (!"api.example.test".equals(uri.getHost())
|| !"/feature-flags".equals(uri.getPath())) {
network.continueRequest(new ContinueRequestParameters(id));
return;
}
List<Header> headers = RequestHeaders.replace(
event.getRequest().getHeaders(), "X-QA-Variant", marker);
if (RequestHeaders.count(headers, "X-QA-Variant") != 1) {
network.continueRequest(new ContinueRequestParameters(id));
return;
}
requestId.set(id);
network.continueRequest(new ContinueRequestParameters(id).headers(headers));
});
network.onResponseCompleted(event -> {
if (event.getRequest().getRequestId().equals(requestId.get())) {
completedId.complete(event.getRequest().getRequestId());
}
});
driver.findElement(By.id("load-feature-flags")).click();
assertEquals(requestId.get(), completedId.get(5, TimeUnit.SECONDS));
new WebDriverWait(driver, Duration.ofSeconds(5)).until(d ->
d.findElement(By.id("server-variant")).getText().equals(marker));
driver.findElement(By.id("load-control")).click();
String controlValue = driver.findElement(By.id("control-variant")).getAttribute("data-value");
assertNull(controlValue);
} finally {
network.removeIntercept(interceptId);
}
}
} finally {
driver.quit();
}
}
}The fixture in this example must display what the server observed, not the marker already known to JavaScript. A test-only endpoint can return an allowlisted observedVariant field or expose a case-scoped server record. The browser response and UI then connect the modified request to an external boundary. Do not return raw headers, cookies, or credentials from a diagnostic endpoint.
Correlate the Mutation with Server and Product Evidence
Store the request ID before sending the continuation. Match onResponseCompleted or onFetchError against that exact ID, not only the URL. Also retain context ID, redirect count, method, sanitized origin and path, intended field name, whether the original field existed, replacement count, terminal event, and cleanup status. The value itself is usually unnecessary; a case marker hash can provide correlation without exposing content.
The blocked event describes the pre-continuation request. It cannot prove that the final replacement list reached the server. This distinction matters during review: inspecting event.getRequest().getHeaders() after constructing a new list still shows input evidence. Use the server echo, server log keyed by a synthetic marker, or a product response that only the intended header can select.
Redirects can change origin after continuation. Decide whether the header is allowed on the initial request only, and scope the intercept accordingly. A new redirect hop may have related events and a redirect count, but do not assume a secret field should follow. The browsing-context routing article adds another dimension when popups or embedded contexts initiate the call.
When payload evidence is also required, correlate it under the same request ID and apply the retention limits in the Selenium response-body guide. Header tests rarely need entire response bodies. Prefer a small typed response field and a visible product assertion so reports stay readable and less sensitive.
Keep callback outcome separate from test verdict. The callback can publish a small immutable record through a future or thread-safe queue. The test thread waits with a deadline, checks any callback exception, then performs UI and server assertions. This avoids WebDriver re-entry from the listener and produces a failure at the correct layer.
For suites that already route several event domains, compare this ownership model with the BiDi WebSocket subscription pattern. Header mutation still needs a test-local intercept and decision ledger even when the underlying session transport is managed by a wider fixture.
Use Negative Controls for Scope and Secrecy
Begin with a no-intercept baseline. The controlled endpoint should report the header absent or the default variant. Then run the modified case and require exactly one case-scoped value. Without this baseline, a proxy, service worker, browser policy, or server default could supply the same result and make the BiDi code irrelevant.
Send a near-match request such as /feature-flags-health and a same-host control such as /account-summary. Neither may receive X-QA-Variant. If the application creates two target requests, define whether both should be modified; an accidental second match should fail a single-request case. Count blocked events, continuations, correlated responses, and server observations.
Test mixed-case duplicates in the incoming list with the pure helper. The result should contain one intended field. Test an invalid marker before browser setup so the listener never owns malformed input. Add a callback-failure case in a small harness and prove the fallback path continues the request unchanged, records the error, and fails the test rather than leaving navigation paused.
Credentials deserve a separate negative policy. Assert that the allowlisted field name is not Authorization, Proxy-Authorization, Cookie, or Set-Cookie. If a specialized security test has approved use of one, keep it in a separate fixture with exact host validation, a secret provider, redacted evidence, and minimal account privileges. Generic header utilities should reject those names by default.
Parity checks can expose browser differences in emitted header casing, duplicate representation, or blocked-event timing. Normalize only what the HTTP contract treats as equivalent, and retain raw field names in short-lived local diagnostics when needed. The CDP and BiDi event parity article provides a structure for comparison without turning one browser's payload shape into the universal assertion.
Debug Missing, Duplicate, or Stalled Continuations
If no event arrives, confirm the WebSocket capability, provider support, listener registration order, context scope, and URL pattern. If an event arrives with isBlocked() false, observation works but the intercept did not match. Compare parsed scheme, host, port, exact path, and query treatment. Log only the sanitized location and pattern decision.
If the event is blocked and the page hangs, inspect the callback ledger. A thrown URI parser, invalid header, failed assertion, early return, or WebDriver call can prevent continuation. Put callback work in a small try/catch that records the original cause and attempts one safe unchanged continuation for the owned request. The test thread should then fail with the callback cause. Never loop terminal commands until one appears to work.
Duplicate server values usually come from appending without removing, repeated original fields, two active listeners, or a second mutation layer. Compare the original count, replacement count, continuation count, and server count. A no such request error after one successful continuation suggests a duplicate decision or stale request ID. An invalid argument points toward a bad field, value, URL, method, or phase.
Missing original behavior after mutation often means the replacement list discarded required headers. Compare names and counts between the pre-continuation list and constructed list, with sensitive values removed. Cookies should remain managed through their intended mechanism. CORS failures can also appear when a custom header triggers a preflight that the fixture did not expect; test and document that browser behavior rather than weakening the assertion.
Use the BiDi WebSocket subscription guide to verify listener activation and teardown order. Your timeline should show registration before trigger and no events after closure. A long page timeout cannot identify a blocked request, while a phase, request ID, decision, and terminal event can.
Own Cleanup and Parallel CI Behavior
Each CI worker needs its own browser session, test account or case fixture, marker, intercept ID, request-ID ledger, and evidence buffer. Never share a static Network instance. Session-local identifiers can look valid in another thread but refer to unrelated protocol state. Isolated ownership also lets cleanup failures be assigned to the exact case.
Remove the intercept in finally while the module and driver are alive. Then close Network so its listeners are cleared, and quit the driver last. If the session dies before removal, retain the primary failure and record cleanup as incomplete. Do not let a teardown exception replace the callback or product failure that caused the session loss.
Pin Selenium Java, browser, driver or Grid image, and provider configuration. Run a compatibility smoke test that modifies one harmless field against a local controlled endpoint. For the full Selenium BiDi request header modification CI pipeline, separate setup failures, callback failures, protocol errors, server assertion failures, UI failures, and cleanup failures. Each class needs a different owner and retry policy.
Bound waits for blocked event, continuation completion, response event, server record, and UI state. Report their durations separately. A retry is acceptable only for a pre-classified transient platform condition, and the first run's event ledger must remain attached. Never retry a header leakage, wrong-host match, duplicate value, or missing cleanup into a pass.
Keep artifacts small: versions, case ID, context, sanitized path, request ID, redirect count, original field count, replacement count, terminal event, server-observed hash, control result, and cleanup result. The Selenium BiDi interview question set is a useful review prompt for explaining which signal proves each boundary and which signals remain assumptions.
Frequently Asked Questions
Does continueRequest add one header or replace all request headers?
When headers are supplied to continueRequest, they represent the request's replacement header list. Copy the original event headers, remove every case-insensitive occurrence of the field being changed, add one intended value, and continue with that complete list. Omitting required originals can change cookies, content negotiation, tracing, or CORS behavior.
How can a test prove the modified header reached the server?
Do not treat the callback's constructed list as delivery evidence. Use a controlled endpoint that returns an allowlisted echo or records a test marker, correlate its response with the blocked request ID, and assert the resulting application state. Also send a control request that must arrive without the injected header.
Which phase is valid for Selenium BiDi request header modification?
Use a beforeRequestSent intercept and call continueRequest for the request ID from that blocked event. A responseStarted event is too late to change outgoing headers, while authRequired has a separate continuation contract. Sending the right command at the wrong phase should be classified as a test implementation failure.
Can I inject Authorization or Cookie with a generic header interceptor?
Avoid generic credential injection. Use browser cookie APIs for cookie state and the authentication-required flow for browser HTTP authentication where applicable. If a product contract truly requires a test authorization header, restrict host, path, account, and artifact access, source the value from secrets, and never retain its contents.
Why does a request hang after I modify its headers?
The event was blocked, but no valid terminal continuation completed. Common causes include an exception while copying headers, an early return for an unexpected URL, a duplicate handler, a phase mismatch, or waiting on WebDriver inside the callback. Capture callback errors and give every matched request exactly one bounded decision.
How should header-modification tests behave in CI?
Run each test in an isolated BiDi session with pinned Selenium and browser versions, unique markers, bounded evidence, and owned cleanup. Gate on duplicate header values, missing server proof, wrong request correlation, control contamination, or intercept removal failure. Keep first-attempt diagnostics even when a classified infrastructure retry is permitted.
Practice a Header Contract in QABattle
Open the QABattle battle catalog and choose a browser scenario whose result can be selected by a harmless test header. Define the allowed host and path, write the baseline without mutation, add one request-scoped continuation, and prove the server observed the marker. Include a same-host control that must remain clean.
Then review the submission with the command-event correlation guide. A strong solution can explain the original header list, replacement policy, request ID, terminal response, product assertion, negative control, and cleanup order without exposing any sensitive value. That evidence is what turns a header trick into a dependable automation check.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 02Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 03Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 04Official w3c.github.io reference
w3c.github.io
Primary documentation selected and verified for the claims in this guide.
FAQ / QUICK ANSWERS
Questions testers ask
Does continueRequest add one header or replace all request headers?
When headers are supplied to continueRequest, they represent the request's replacement header list. Copy the original event headers, remove every case-insensitive occurrence of the field being changed, add one intended value, and continue with that complete list. Omitting required originals can change cookies, content negotiation, tracing, or CORS behavior.
How can a test prove the modified header reached the server?
Do not treat the callback's constructed list as delivery evidence. Use a controlled endpoint that returns an allowlisted echo or records a test marker, correlate its response with the blocked request ID, and assert the resulting application state. Also send a control request that must arrive without the injected header.
Which phase is valid for Selenium BiDi request header modification?
Use a beforeRequestSent intercept and call continueRequest for the request ID from that blocked event. A responseStarted event is too late to change outgoing headers, while authRequired has a separate continuation contract. Sending the right command at the wrong phase should be classified as a test implementation failure.
Can I inject Authorization or Cookie with a generic header interceptor?
Avoid generic credential injection. Use browser cookie APIs for cookie state and the authentication-required flow for browser HTTP authentication where applicable. If a product contract truly requires a test authorization header, restrict host, path, account, and artifact access, source the value from secrets, and never retain its contents.
Why does a request hang after I modify its headers?
The event was blocked, but no valid terminal continuation completed. Common causes include an exception while copying headers, an early return for an unexpected URL, a duplicate handler, a phase mismatch, or waiting on WebDriver inside the callback. Capture callback errors and give every matched request exactly one bounded decision.
How should header-modification tests behave in CI?
Run each test in an isolated BiDi session with pinned Selenium and browser versions, unique markers, bounded evidence, and owned cleanup. Gate on duplicate header values, missing server proof, wrong request correlation, control contamination, or intercept removal failure. Keep first-attempt diagnostics even when a classified infrastructure retry is permitted.
RELATED GUIDES
Continue the learning route
GUIDE 01
Selenium WebDriver BiDi Interview Questions
Selenium BiDi interview questions advanced: practical design, implementation, debugging, CI, metrics, and interview guidance for QA, SDET, and automation engineers.
GUIDE 02
Intercept Requests and Supply Authentication with Selenium BiDi Network
Use Selenium BiDi network intercepts for scoped Basic or Digest authentication, request control, response evidence, cleanup, and safe diagnostics.
GUIDE 03
Migrate Selenium CDP Hooks to WebDriver BiDi
Migrate Selenium CDP hooks to WebDriver BiDi with a capability inventory, event adapters, parity tests, cross-browser rollout, and failure diagnostics.
GUIDE 04
Compare CDP and WebDriver BiDi Event Parity
A practical guide to Selenium CDP BiDi event parity testing, with implementation examples, debugging workflows, CI evidence, security controls, and release gates.
GUIDE 05
Combine Classic WebDriver Commands with BiDi Events
Combine Classic WebDriver Commands with BiDi Events: practical implementation, debugging, evidence, security, CI, and release guidance for QA teams.