PRACTICAL GUIDE / debug Selenium BiDi network intercept stuck

Debug Selenium BiDi Network Intercepts That Never Resume

Learn debug Selenium BiDi network intercept stuck with working code, failure cases, debugging steps, and CI evidence for reliable QA automation.

By The Testing AcademyUpdated July 18, 202618 min read
All field guides
In this guide11 sections
  1. Recognize the Blocked-Request Lifecycle
  2. Classify the Last Transition Before Changing Timeouts
  3. Verify the API Surface and Phase Rules
  4. Instrument a Request-ID Transition Ledger
  5. Reproduce with a Known-Good Continuation Workflow
  6. Correlate Phase, Context, Redirect, and Request ID
  7. Use Negative Controls to Prove the Watchdog Works
  8. Diagnose Callback Deadlocks and Rejected Decisions
  9. Clean Up Ownership and Define CI Behavior
  10. Frequently Asked Questions
  11. How can I tell a deliberate network block from a stuck intercept?
  12. Why does removeIntercept not release the request already waiting?
  13. Should a BiDi callback call WebDriver to inspect the page?
  14. Will a longer page-load timeout fix a stuck network intercept?
  15. What usually causes no such request during continuation?
  16. How should a stuck-intercept failure be cleaned up in CI?
  17. Practice Incident Diagnosis in QABattle

What you will learn

  • Recognize the Blocked-Request Lifecycle
  • Classify the Last Transition Before Changing Timeouts
  • Verify the API Surface and Phase Rules
  • Instrument a Request-ID Transition Ledger

To debug Selenium BiDi network intercept stuck failures, find the first missing transition: listener active, intercept matched, blocked event received, one phase-valid decision accepted, terminal event observed, and cleanup completed. Correlate every step by request ID, surface callback exceptions on the test thread, never call WebDriver from the callback, and treat unresolved requests as owned teardown work.

Recognize the Blocked-Request Lifecycle

A page spinner or navigation timeout is the last symptom, not the first cause. WebDriver BiDi intercepts deliberately pause network processing at beforeRequestSent, responseStarted, or authRequired. Once a matching event reports isBlocked=true, normal processing waits for a phase-compatible command. If the callback returns without one, throws before sending it, or sends a rejected command, the browser is doing what the intercept asked: waiting.

Write the expected lifecycle as observable transitions. The listener becomes active before the trigger. addIntercept returns an ID. A matching event includes that ID, a request ID, context, phase, and redirect count. The callback records its decision, the network command returns or throws, a terminal event follows, the product reaches a state, the intercept is removed, listeners close, and the session quits. The command-event correlation article explains why these are separate clocks.

The blocked event is the ownership boundary. An ordinary event with isBlocked=false needs no continuation from your listener. A blocked event listing your intercept ID does. If it lists another owner's intercept, do not guess which component should resume it. Shared control is the fastest route to duplicate decisions and no such request errors.

The W3C WebDriver BiDi network specification states that no further processing occurs while a before-request intercept waits. It also states that removing an intercept does not affect requests already blocked by it. Those two rules explain many teardown hangs: removal prevents future matches but is not a release command for current request IDs.

Treat the listener, intercept, decision ledger, and browser session as asynchronous owned resources. Their owner must know when observation started, which IDs remain unresolved, and how cleanup behaves after a partial failure. A global callback with no registration handle or case identity cannot answer those questions during an incident.

Classify the Last Transition Before Changing Timeouts

Start with the last trustworthy marker and choose the next probe from it. This keeps diagnosis falsifiable and prevents random timeout changes from obscuring the original state.

Last observed stateLikely boundaryNext evidence to inspectUnsafe reaction
No network eventCapability, subscription, context, or triggerWebSocket setup, listener time, sanitized targetIncrease page timeout
Event with isBlocked=falseIntercept phase or pattern missIntercept ID, exact URL parts, contextSend a continuation anyway
Blocked event, no callback exitCallback deadlock or slow dependencyThread dump, callback start, forbidden WebDriver callAdd a sleep inside callback
Callback error, no commandPolicy or data handlingCaptured throwable and request IDLet event-thread error disappear
Command rejectedWrong phase, ID, or duplicate ownerError code, phase, decision historyRetry terminal commands blindly
Decision accepted, no terminal eventTransport, browser, or later phaseBiDi connection, response or fetch-error listenerReuse the session
Test passed, teardown hangsUnresolved extra match or cleanup orderMatch count, unresolved IDs, removal resultForce fixture reuse

An intercept can match more requests than the test expects. Preflight calls, redirects, favicon loads, retries, frames, and a second click can each add an owned request ID. Count blocked events and decisions. One expected request with two blocked IDs is a scope defect even if the first response lets the UI pass.

Authentication requires a distinct terminal contract. The Selenium BiDi auth-intercept guide should be the reference for authRequired, credential continuation, default handling, and cancellation. A generic continueRequest strategy cannot safely resolve every phase.

Separate product progress from network progress. A completed continuation can still lead to a server delay or broken UI assertion. Conversely, a visible fallback might be pre-rendered while the target request remains blocked. Request-ID events and product state must agree before the incident is classified.

Verify the API Surface and Phase Rules

Examples in this article use Selenium Java 4.44.0 signatures verified against the tagged binding source on July 18, 2026. That version exposes request and response continuations, request failure, authentication continuations, intercept removal, and network listeners through Network. Pin the Java client, browser, driver or Grid image, and provider configuration when reproducing a hang.

Enable webSocketUrl before session creation. The Selenium BiDi overview describes the bidirectional channel. A successful classic command does not confirm that the remote BiDi connection remains alive. Record session creation, BiDi capability, listener registration, and a known probe event before registering the failing intercept.

The official Selenium W3C network page shows Java intercept and event APIs. Match commands to phases: continueRequest or failRequest at BEFORE_REQUEST_SENT, continueResponse at RESPONSE_STARTED, and authentication continuation at AUTH_REQUIRED. The same request ID can appear in several lifecycle events, but phase still controls which command is valid.

Check event.isBlocked() and event.getIntercepts().contains(interceptId) before acting. An observer sees unblocked events too. Sending a continuation for an unblocked request should fail because the remote blocked-request map has no entry. That error is useful evidence of a test bug, not a reason to suppress protocol errors broadly.

The Selenium network features documentation tracks higher-level handlers, but language support and examples evolve. Confirm the installed class signatures rather than copying a method from another binding. Avoid reflection, version guessing, and broad exception handling around all network commands; each can convert a clear compatibility failure into a stuck page.

Instrument a Request-ID Transition Ledger

A ledger should be smaller than a network log and more precise than a screenshot. For each owned request ID, store context, phase, intercept ID, sanitized URL, redirect count, callback entry, intended decision, command result, terminal event, and cleanup result. Use monotonic time for durations and wall-clock time only for cross-system correlation.

Do not log complete URLs, headers, credentials, cookies, or bodies by default. Store origin and path, field names, counts, and hashes when needed. Authentication challenge details and private API routes need stricter retention. The aim is to explain a state transition without creating another security incident.

Java example 1: enforce legal local decision transitions

Java
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

enum InterceptState { BLOCKED, DECIDING, CONTINUED, TERMINAL, CALLBACK_ERROR }

record InterceptTransition(
    String requestId,
    InterceptState state,
    long nanoTime,
    String detail) {}

final class InterceptLedger {
  private final ConcurrentMap<String, InterceptState> current =
      new ConcurrentHashMap<>();
  private final List<InterceptTransition> transitions =
      new CopyOnWriteArrayList<>();

  void blocked(String requestId) {
    if (current.putIfAbsent(requestId, InterceptState.BLOCKED) != null) {
      throw new IllegalStateException("Duplicate blocked request: " + requestId);
    }
    add(requestId, InterceptState.BLOCKED, "event received");
  }

  void deciding(String requestId, String decision) {
    move(requestId, InterceptState.BLOCKED, InterceptState.DECIDING, decision);
  }

  void continued(String requestId) {
    move(requestId, InterceptState.DECIDING, InterceptState.CONTINUED, "command returned");
  }

  void terminal(String requestId, String eventName) {
    move(requestId, InterceptState.CONTINUED, InterceptState.TERMINAL, eventName);
  }

  void callbackError(String requestId, Throwable error) {
    current.put(requestId, InterceptState.CALLBACK_ERROR);
    add(requestId, InterceptState.CALLBACK_ERROR, error.getClass().getSimpleName());
  }

  Set<String> unresolvedBlockedIds() {
    return current.entrySet().stream()
        .filter(entry -> entry.getValue() == InterceptState.BLOCKED
            || entry.getValue() == InterceptState.DECIDING
            || entry.getValue() == InterceptState.CALLBACK_ERROR)
        .map(java.util.Map.Entry::getKey)
        .collect(Collectors.toUnmodifiableSet());
  }

  List<InterceptTransition> snapshot() {
    return List.copyOf(transitions);
  }

  private void move(
      String requestId, InterceptState expected, InterceptState next, String detail) {
    if (!current.replace(requestId, expected, next)) {
      throw new IllegalStateException(
          "Expected " + expected + " for " + requestId + ", found " + current.get(requestId));
    }
    add(requestId, next, detail);
  }

  private void add(String requestId, InterceptState state, String detail) {
    transitions.add(new InterceptTransition(requestId, state, System.nanoTime(), detail));
  }
}

This state machine does not replace remote truth. A local CONTINUED marker means the Selenium command returned, while TERMINAL requires a browser event. Keeping both exposes transport loss and incorrect assumptions. Cap the ledger, attach it only when useful, and test the transition class without a browser.

Callback exceptions must cross to the test thread. Store the first throwable in an atomic reference or complete a future exceptionally. The test should check it before reporting a generic event timeout. If several callbacks fail, retain counts and suppressed summaries without flooding artifacts.

Reproduce with a Known-Good Continuation Workflow

Build a minimal controlled request before debugging the production path:

  1. Start a fresh BiDi-enabled session and prove one ordinary response event arrives.
  2. Open a same-origin fixture that does not issue the target request during page load.
  3. Add one exact BEFORE_REQUEST_SENT intercept and retain its ID.
  4. Register the blocked-event and completion listeners before clicking the fetch button.
  5. Record the request ID, move the ledger to deciding, and send one unchanged continueRequest.
  6. Require command return, correlated responseCompleted, and a visible fixture result under separate deadlines.
  7. Run a nearby unblocked control request and verify no extra ledger entry appears.
  8. Resolve any known blocked IDs, remove the intercept, close listeners, and quit the session.

Java example 2: expose callback failure and clean known blocked IDs

Java
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.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.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 InterceptProgressTest {
  @Test
  void requestMovesFromBlockedToCompleted() throws Exception {
    ChromeOptions options = new ChromeOptions();
    options.setCapability("webSocketUrl", true);
    WebDriver driver = new ChromeDriver(options);
    InterceptLedger ledger = new InterceptLedger();
    AtomicReference<Throwable> callbackFailure = new AtomicReference<>();

    try {
      driver.get("https://app.example.test/intercept-lab");
      CompletableFuture<String> blocked = new CompletableFuture<>();
      CompletableFuture<String> continued = new CompletableFuture<>();
      CompletableFuture<String> completed = 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("app.example.test")
                    .pathname("/api/probe")));
        try {
          network.onBeforeRequestSent(event -> {
            if (!event.isBlocked() || !event.getIntercepts().contains(interceptId)) {
              return;
            }
            String id = event.getRequest().getRequestId();
            try {
              ledger.blocked(id);
              blocked.complete(id);
              URI uri = URI.create(event.getRequest().getUrl());
              if (!"app.example.test".equals(uri.getHost())
                  || !"/api/probe".equals(uri.getPath())) {
                throw new IllegalStateException("Unexpected intercepted target");
              }
              ledger.deciding(id, "continueRequest unchanged");
              network.continueRequest(new ContinueRequestParameters(id));
              ledger.continued(id);
              continued.complete(id);
            } catch (Throwable error) {
              ledger.callbackError(id, error);
              callbackFailure.compareAndSet(null, error);
            }
          });

          network.onResponseCompleted(event -> {
            String id = event.getRequest().getRequestId();
            if (id.equals(blocked.getNow(null))) {
              completed.complete(id);
            }
          });

          driver.findElement(By.id("run-probe")).click();
          String id = blocked.get(2, TimeUnit.SECONDS);
          assertEquals(id, continued.get(2, TimeUnit.SECONDS), ledger.snapshot().toString());
          assertEquals(id, completed.get(5, TimeUnit.SECONDS), ledger.snapshot().toString());
          ledger.terminal(id, "responseCompleted");
          new WebDriverWait(driver, Duration.ofSeconds(5)).until(d ->
              d.findElement(By.id("probe-result")).getText().equals("probe-ok"));
          assertNull(callbackFailure.get(), ledger.snapshot().toString());
        } finally {
          for (String unresolvedId : ledger.unresolvedBlockedIds()) {
            try {
              network.failRequest(unresolvedId);
            } catch (RuntimeException cleanupError) {
              Throwable original = callbackFailure.get();
              if (original != null) {
                original.addSuppressed(cleanupError);
              }
            }
          }
          network.removeIntercept(interceptId);
        }
      }
    } finally {
      driver.quit();
    }
  }
}

The best-effort cleanup is phase-specific because this example owns only a before-request intercept. Do not copy failRequest as a universal resolver for response or authentication phases. If command outcome is uncertain, record it and dispose of the session. A pooled browser with unknown blocked state is not safe to return to another test.

Correlate Phase, Context, Redirect, and Request ID

URL filters find candidates; request IDs track ownership. Store session ID, browsing context, request ID, phase, redirect count, and intercept ID as the event key. A global map keyed only by path can join an event from another tab, retry, redirect hop, or parallel test. That produces both missing continuations and duplicate terminal commands.

Redirects deserve an explicit timeline. A request can produce repeated lifecycle events with changing redirect count, and the intended phase may match more than one hop. Decide whether the intercept targets the initial URL or final resource. Record all matched IDs and counts, then continue each owned event once. Do not keep only the last URL seen.

Contexts also change. A popup, frame, or replaced tab may issue the target while the test listens to another top-level owner. The BiDi browsing-context mutation guide helps map creation and destruction events to network ownership. A context-scoped Network module reduces volume but does not remove the need to store context evidence.

Keep command IDs and request IDs conceptually separate. The protocol command response confirms whether Selenium's continuation command succeeded; the network request ID connects browser lifecycle events. Reusing one label for both makes incident logs hard to interpret. Name fields requestId, interceptId, and decisionResult explicitly.

When body evidence is needed after a continuation, use the limits described in the response-body capture article. A stuck-intercept incident usually needs only lifecycle metadata. Collecting every body and header during a hang can increase memory pressure, expose secrets, and bury the first missing transition.

Use Negative Controls to Prove the Watchdog Works

Run the fixture without an intercept first. The request and product state should complete, proving that the server and page are healthy. Then add the intercept with an unchanged continuation and require the full ledger. If only the intercepted run hangs, the problem is in matching, callback execution, command handling, or teardown rather than the product route.

Add a pattern-miss case. A near-match request should produce an observable unblocked event but no ledger entry owned by the intercept. Add an extra-match case that intentionally creates two requests and require two separate decisions. These controls catch code that completes the first future and silently abandons the second blocked ID.

Inject a policy exception before sending the terminal command in a disposable session. The test must surface the original throwable on its test thread, record the blocked ID, attempt phase-valid cleanup, and refuse session reuse. This validates incident behavior without waiting for a real callback bug. Bound the test so it cannot consume the full CI job timeout.

Register a duplicate callback in a dedicated framework test and verify the ledger rejects the second ownership attempt. Do not run duplicate terminal commands against an important environment. The desired outcome is a clear framework failure showing both registrations, not a random race where one command succeeds and the other reports no such request.

After removal, trigger a sentinel request to the former target and prove it is unblocked and completes. This checks future matching, while the unresolved-ID ledger checks requests blocked before removal. The CDP and BiDi event parity article offers a useful normalization pattern when reproducing a migration-only stall across separate sessions.

Diagnose Callback Deadlocks and Rejected Decisions

Never invoke driver.get, element lookup, JavaScript execution, a wait condition, or another classic WebDriver command from the network callback. The command can wait for document or network progress that the current callback has paused. The event thread then waits on WebDriver while WebDriver waits on the event's terminal decision. Move all DOM work to the test thread.

Avoid slow secret providers, database calls, remote logging, or locks in callbacks. Resolve configuration before adding the intercept. Make policy selection a pure in-memory function, send the decision, publish a small record, and return. If external evidence is needed, enqueue it for another thread after continuation.

Capture a JVM thread dump when the callback-start marker exists but no decision marker appears. Identify the BiDi event thread, the test thread, and any lock owner. A callback blocked inside findElement, WebDriverWait, synchronous artifact upload, or a shared framework monitor gives you a concrete cycle to remove. Take the dump before interrupting the test so the wait graph remains intact. Pair it with the ledger timestamp and a sanitized request ID; a thread name alone may be reused across sessions. When the callback is merely slow, measure the local policy step separately and move nonessential work out of that path rather than expanding the browser timeout.

An early return is valid only for an unblocked event or an event not owned by the intercept. Every owned blocked branch needs one terminal command. Review if, switch, exception, cancellation, and validation paths. A guard that rejects an unexpected URL but simply returns is itself a stuck-intercept defect; continue unchanged or fail according to a predeclared safe policy, then fail the test separately.

no such request usually means the ID is stale, already resolved, or from another session. invalid argument commonly indicates a phase mismatch or invalid continuation data. Capture the exact error class and message with sanitized IDs. Do not catch every runtime exception and label it a browser timeout.

A lost command response creates a different uncertainty: the remote browser may have accepted the continuation even though the client never observed success. Do not immediately send another terminal command. Mark the decision outcome unknown, watch briefly for a correlated terminal event, and contain the session if neither signal arrives. The command-event correlation guide helps represent this three-state result: accepted, rejected, or unknown. Cleanup can attempt a best-effort phase-valid action for a request still known to be blocked, but the browser must remain disposable because a duplicate action and a still-paused request are both possible. Preserve provider transport logs and the first disconnect marker for assignment.

For transport diagnosis, use the BiDi WebSocket subscription guide to inspect connection and listener lifetime. A command that returned but never produced any later event may indicate a disconnected channel, browser failure, or listener removal. Compare the last successful probe event and provider logs before changing application code.

Clean Up Ownership and Define CI Behavior

Cleanup order matters. First inspect the ledger for blocked or deciding request IDs. Send the phase-valid terminal action where outcome is known. Next remove the intercept so future traffic cannot match. Then close the Network module to clear listeners, and quit the browser. If any step fails, retain the original incident and append cleanup evidence rather than replacing the root cause.

Removing the intercept first leaves current blocked requests waiting. Closing listeners before resolving them removes the local code capable of making a decision. Quitting the session is the final containment action when remote state is uncertain. Never return such a browser to a pool, even when a sentinel page appears responsive.

The debug Selenium BiDi network intercept stuck CI pipeline needs separate deadlines for event arrival, callback decision, terminal event, product state, and teardown. Report which deadline expired and include the transition snapshot. A single 60-second page timeout makes all failure classes look alike and delays every feedback loop.

Give each parallel worker a dedicated session, case ID, intercept ID, ledger, and bounded artifact. Pin Selenium, browser, driver or Grid image, and provider settings. Classify unsupported BiDi, listener loss, policy exception, protocol rejection, browser failure, product delay, and cleanup failure separately. A retry is allowed only for a named transient infrastructure class and must preserve first-attempt evidence.

Gate on unresolved IDs, duplicate decisions, callback WebDriver use, wrong-context correlation, lost terminal events, or failed cleanup. The Selenium BiDi interview questions can help reviewers practice explaining why removal is not release and why command return is not a terminal browser event. Those distinctions should be visible in code review, not learned during an outage.

Frequently Asked Questions

How can I tell a deliberate network block from a stuck intercept?

A deliberate block reaches an expected terminal command and downstream evidence such as fetchError or response completion. A stuck intercept has a blocked event but no accepted terminal decision. Record phase, intercept ID, request ID, callback start, command result, and terminal event so a page timeout is not the only symptom.

Why does removeIntercept not release the request already waiting?

The protocol defines removal for future matches and future phases; it does not resolve a request already present in the blocked-request map. Send the valid phase-specific terminal command for every known blocked request first, then remove the intercept. If command outcome is uncertain, preserve that ambiguity and end the disposable session.

Should a BiDi callback call WebDriver to inspect the page?

No. The WebDriver command may wait for a page whose network request is paused by the callback, creating a circular wait. Read immutable event data, choose and send one network decision, publish small evidence, and return. Perform DOM checks on the test thread after continuation or terminal-event evidence arrives.

Will a longer page-load timeout fix a stuck network intercept?

Only if evidence shows valid continuation followed by slow progress. A timeout cannot repair an early callback return, thrown exception, duplicate decision, wrong phase, stale request ID, or lost BiDi transport. Use separate deadlines for blocked event, decision completion, terminal event, and product state to identify the first missing transition.

What usually causes no such request during continuation?

The request ID may belong to another session, another request may have been correlated by URL, a previous handler may already have resolved it, or the callback may be issuing a duplicate command. Keep one owner per intercept and a state transition ledger keyed by session, context, request ID, and phase.

How should a stuck-intercept failure be cleaned up in CI?

Attempt a valid terminal decision for each known unresolved request, record cleanup errors without replacing the original failure, remove the intercept, close listeners, and quit the disposable session. Never return that browser to a shared pool. Attach the transition ledger and dependency versions before any classified infrastructure retry begins.

Practice Incident Diagnosis in QABattle

Open the QABattle battle catalog and select a browser flow with a controlled fetch. Run it ordinarily, then add an unchanged continuation and instrument every transition. Create one disposable case that throws before the decision and show that the ledger, cleanup, and test verdict identify the real boundary without waiting for the job timeout.

Review the result with the command-event correlation guide, then compare migration assumptions in the CDP to BiDi guide. A strong incident exercise can name the last completed state, unresolved request owner, phase-valid recovery, negative control, and reason the session is or is not safe to reuse.

// 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.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 18, 2026 / Reviewed July 18, 2026

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.

  1. 01
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Official w3c.github.io reference

    w3c.github.io

    Primary documentation selected and verified for the claims in this guide.

FAQ / QUICK ANSWERS

Questions testers ask

How can I tell a deliberate network block from a stuck intercept?

A deliberate block reaches an expected terminal command and downstream evidence such as fetchError or response completion. A stuck intercept has a blocked event but no accepted terminal decision. Record phase, intercept ID, request ID, callback start, command result, and terminal event so a page timeout is not the only symptom.

Why does removeIntercept not release the request already waiting?

The protocol defines removal for future matches and future phases; it does not resolve a request already present in the blocked-request map. Send the valid phase-specific terminal command for every known blocked request first, then remove the intercept. If command outcome is uncertain, preserve that ambiguity and end the disposable session.

Should a BiDi callback call WebDriver to inspect the page?

No. The WebDriver command may wait for a page whose network request is paused by the callback, creating a circular wait. Read immutable event data, choose and send one network decision, publish small evidence, and return. Perform DOM checks on the test thread after continuation or terminal-event evidence arrives.

Will a longer page-load timeout fix a stuck network intercept?

Only if evidence shows valid continuation followed by slow progress. A timeout cannot repair an early callback return, thrown exception, duplicate decision, wrong phase, stale request ID, or lost BiDi transport. Use separate deadlines for blocked event, decision completion, terminal event, and product state to identify the first missing transition.

What usually causes no such request during continuation?

The request ID may belong to another session, another request may have been correlated by URL, a previous handler may already have resolved it, or the callback may be issuing a duplicate command. Keep one owner per intercept and a state transition ledger keyed by session, context, request ID, and phase.

How should a stuck-intercept failure be cleaned up in CI?

Attempt a valid terminal decision for each known unresolved request, record cleanup errors without replacing the original failure, remove the intercept, close listeners, and quit the disposable session. Never return that browser to a shared pool. Attach the transition ledger and dependency versions before any classified infrastructure retry begins.