skip to content

Network Event Capture

Subscribing to requests, responses and console entries while they happen, over WebDriver BiDi or the older Chrome-only DevTools bridge. Interviewers probe the cross-browser cost.

on this pageshow

explore

questions

3

In Selenium 4, why must a BiDi network or console listener be registered before the step you are diagnosing?

level: juniorimportance: should knowfreq 41%

answer

  1. A tap you open, not a query
  2. Nothing earlier is replayed
  3. The browser starts sending when you subscribe
  4. session.subscribe fires as the listener is added
  5. Register in setup, not in the catch block

basics

~10 s

Selenium's BiDi events are push-only. Subscribing sends a session.subscribe command and the browser starts delivering from that moment; nothing earlier is replayed. A listener added after the failing click captures nothing about it.

solid answer

~40 s

In Selenium 4, registering a BiDi listener — `new Network(driver).onResponseCompleted(...)` or `new LogInspector(driver).onConsoleEntry(...)` — sends a `session.subscribe` command to the browser, and events flow only from that point on. There is no buffer to drain and no replay command, so a subscription opened in a failure handler sees an empty stream for the step that already failed. On a gym class-booking board, the listener meant to prove that `POST /api/bookings` answered 500 has to exist before the Book button is clicked, normally in the per-test setup. Collect the records into a thread-safe field the test can read afterwards, because the consumer runs on a Selenium BiDi connection thread rather than the test thread, and assert on that collection once the step is done.

code

java · 37 lines
java
import java.util.concurrent.CopyOnWriteArrayList;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.bidi.module.Network;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;

public class BookingBoardEvidence {

  public static void main(String[] args) {
    FirefoxOptions options = new FirefoxOptions();
    options.enableBiDi();
    WebDriver driver = new FirefoxDriver(options);

    CopyOnWriteArrayList<String> failedCalls = new CopyOnWriteArrayList<>();
    try (Network network = new Network(driver)) {
      network.onResponseCompleted(
          response -> {
            if (response.getResponseData().getStatus() >= 400) {
              failedCalls.add(
                  response.getRequest().getMethod()
                      + " "
                      + response.getRequest().getUrl()
                      + " -> "
                      + response.getResponseData().getStatus());
            }
          });
      network.onFetchError(error -> failedCalls.add("fetch error: " + error.getErrorText()));

      driver.get("https://gym.example.com/classes");
      driver.findElement(By.cssSelector("[data-class-id='spin-0700'] button.book")).click();
      System.out.println(failedCalls);
    } finally {
      driver.quit();
    }
  }
}

go deeper

for a junior

Be ready to say that a subscription starts a live stream and replays nothing. Know that the listener goes in setup or immediately before the risky step, never in the code that runs after the failure.

for a middle

Explain the mechanics: adding a listener sends a subscribe command, the browser then pushes events, and closing the module unsubscribes. Be able to name the request and response fields worth keeping and how a request is tied to its response.

for a senior

Show the judgment about where subscription belongs in a real suite: what the capture window should be, why an empty evidence list is more dangerous than none, and how the collection stays safe when the callback runs off the test thread.

for a principal

Own the harness-wide policy: whether every test carries capture or only tagged ones, what the standard evidence projection is, and how the team avoids reports that claim a clean network on a run that failed on a request.

## What a BiDi subscription actually is **WebDriver BiDi** is a bidirectional socket between the Selenium client and the browser. A classic WebDriver command is a request the client sends and the remote end answers; a BiDi **event** has the opposite shape — the browser pushes it to the client without being asked. Selenium 4's Java bindings wrap the two event families a failing test cares about: - `org.openqa.selenium.bidi.module.Network`, whose `onBeforeRequestSent`, `onResponseStarted`, `onResponseCompleted` and `onFetchError` each take a `Consumer` of the matching record. - `org.openqa.selenium.bidi.module.LogInspector`, whose `onConsoleEntry`, `onJavaScriptLog`, `onJavaScriptException`, `onGenericLog` and `onLog` do the same for log entries. Calling one of those methods does two things. It stores your `Consumer` on the client, and it sends a `session.subscribe` command naming the event to the browser. **The browser starts emitting that event only once the subscribe has landed.** Nothing is buffered on your behalf beforehand, and there is no "give me the events I missed" command to follow up with. A subscription is a tap you open, not a query you run. Selenium's older retrieve-a-buffer log API is a different mechanism with different limits, and it is not what a BiDi subscription gives you. ## The sequence that catches people out Picture a gym class-booking board whose Book button silently does nothing for the 07:00 spin class: 1. `driver.get("https://gym.example.com/classes")` loads the board. 2. The test clicks the Book button for the `spin-0700` row. 3. The page fires `POST /api/bookings`, the server answers `500`, and the row never flips to Booked. 4. The assertion on the row's text fails and the failure handler runs. 5. The handler constructs `new Network(driver)` and calls `onResponseCompleted(...)`. Step 5 subscribes hundreds of milliseconds after the response it was meant to capture. The list it fills stays empty, and the report says "no failed requests" for a run that failed *because* of a failed request. That is worse than having no evidence at all, because it is confidently wrong and sends the next reader looking at the wrong layer. ## Where the subscription belongs | Registered where | Sees the failing call? | What it costs | |---|---|---| | Per-test setup, before the first navigation | Yes, including the board's own page-load requests | One subscribe per test; events flow for the whole test | | Immediately before the risky step | Yes, when that step is the only suspect | Cheapest; loses the earlier context of the run | | Inside the failure handler | **No** — the stream begins after the failure | Zero events, and a false "clean run" record | The default is the first row. Subscribe in setup, keep a sink the test can read afterwards, and let the assertion inspect that sink once the step is over. The second row is a reasonable narrowing when a suite is large and only one interaction is under investigation. ## Collecting the records safely The consumer does not run on the thread that called `onResponseCompleted`. Selenium's `org.openqa.selenium.bidi.Connection` dispatches incoming socket frames onto a cached pool of daemon threads named **BiDi Connection**, so your callback executes there while the test thread is still clicking. Three consequences follow: - Collect into a thread-safe sink — a `CopyOnWriteArrayList` or a `ConcurrentLinkedQueue` — not a bare `ArrayList`. - Do not assert inside the consumer. An exception thrown there is raised on a pooled thread and never reaches the test, so a "failing" assertion silently passes. - Do the filtering in the consumer and keep only what you will read: a status of `400` or above, plus every `onFetchError` record. ## What the record actually carries `BeforeRequestSent` and `ResponseDetails` both extend `BaseParameters`, which exposes `getBrowsingContextId()`, `getNavigationId()`, `getRedirectCount()`, `getTimestamp()` and `getRequest()`. `RequestData` gives `getRequestId()`, `getUrl()`, `getMethod()`, `getHeaders()`, `getCookies()` and `getTimings()`; `ResponseData`, reached through `ResponseDetails.getResponseData()`, gives `getStatus()`, `getStatusText()`, `getMimeType()`, `getHeaders()`, `isFromCache()` and `getBytesReceived()`. - `getRequestId()` is what ties a `network.beforeRequestSent` record to the `network.responseCompleted` record for the same call. - The **response body is not in the event**. `ResponseData.getContent()` returns an `Optional<Long>` — a size, not the bytes — so "subscribe to everything" still will not hand you the JSON the booking endpoint returned. ## Closing the tap Both `Network` and `LogInspector` implement `AutoCloseable`. Closing sends `session.unsubscribe` for the events that object subscribed, which matters when a driver is reused across tests: listeners left registered keep firing, and one test's requests land in the next test's evidence. A try-with-resources block around the step under test is the tidiest form, provided the block also contains the interaction — close it too early and you are back to an empty record.

  • Can you narrow a subscription to one browsing context instead of the whole session?
    Yes. `new Network(browsingContextId, driver)` and `new LogInspector(browsingContextId, driver)` subscribe for a named context only, and a `Set<String>` overload takes several. Each delivered record carries `getBrowsingContextId()`, which for a top-level page matches the value `driver.getWindowHandle()` returns, so you can also filter after the fact if you subscribed session-wide.
  • What happens to the subscription when the Network object is closed?
    `Network` and `LogInspector` implement `AutoCloseable`, and `close()` sends `session.unsubscribe` for the events that object subscribed. Delivery stops immediately, so anything the page does afterwards is not recorded. Close in the same scope that opened the module, and make sure the interaction under test happens inside that scope.
  • Does a captured response event give you the response body as evidence?
    No. `ResponseDetails.getResponseData()` exposes `getStatus()`, `getStatusText()`, `getHeaders()`, `getMimeType()`, `isFromCache()` and `getBytesReceived()`, and `getContent()` returns an `Optional<Long>` size rather than the bytes. Capture proves which call failed and with what status; it does not hand you the JSON the booking endpoint returned.

It is a live radio broadcast, not a recording. Tune in after the announcement and the part you needed is simply gone; there is no rewind.

saying these in an interview costs you the question

  • Thinks the browser buffers events and replays them when you subscribe
  • Registers the network listener inside the failure handler, after the click
  • Expects a listener added mid-run to show requests made earlier
  • Treats the event stream as a query that can be run afterwards
  • Collects records into a plain ArrayList from the callback thread
open as a page

Your Selenium suite records failed requests with getDevTools(); what breaks when the same suite runs on Firefox?

level: middleimportance: should knowfreq 53%

basics

~10 s

getDevTools() comes from HasDevTools, which in Selenium 4 only ChromiumDriver implements. On Firefox the cast throws ClassCastException, so the Firefox runs either die or record nothing, leaving that browser's failures with no request evidence.

open as a page

In Selenium 4, what does subscribing to every BiDi network event in every test cost a suite?

level: seniorimportance: nice to knowfreq 31%

basics

~20 s

In Selenium 4, every subscribed BiDi event crosses the socket and is deserialised by the client, and the callback runs on a connection thread, not the test thread. Narrow the events, keep only failures, and unsubscribe at teardown.

open as a page