Your suite gets a Selenium Grid session, then fails at first BiDi use. Why?
answer
- the session succeeded, so nothing threw
- the outcome is written into the reply
- the flag is the default unless you asked
- the client holds an empty optional
- the exception arrives at first use
basics
~20 sSelenium Grid recorded the outcome in the reply rather than in an error. It deleted the socket address from the returned capabilities and set a boolean flag false, so creation still succeeded and the client built no channel at all.
solid answer
~50 sSelenium Grid's `LocalNode` assembles the returned capabilities on both paths. It takes the positive path only when three things hold at once: the Node supports the channel, the Grid has the feature enabled, and the **merged** capabilities carry `webSocketUrl` as a string — which happens when you asked and the browser answered with a real socket address. Then it keeps that socket in `se:gridWebSocketUrl` and returns a rewritten `webSocketUrl` pointing at the Grid, which proxies. Otherwise it filters every `webSocketUrl` key out of the reply and stamps `se:bidiEnabled` `false` — so that flag is also the ordinary reply for a session that never asked, not proof of a refusal. **Creation still succeeds.** `RemoteWebDriver` finds nothing usable and quietly holds an empty optional; the failure surfaces at first use as a `BiDiException` whose message points at your *request*. The DevTools bridge is governed the same way, through `se:cdpEnabled`.
code
java · 24 lines// Selenium Grid, LocalNode#createExternalSession - the negative path.
// The session is still created; the channel is simply not there. This is
// also the branch a session that never asked for the channel takes.
MutableCapabilities bidiFiltered = new MutableCapabilities();
toUse
.asMap()
.forEach(
(key, value) -> {
if (!key.startsWith("webSocketUrl")) {
bidiFiltered.setCapability(key, value);
}
});
toUse = new PersistentCapabilities(bidiFiltered).setCapability("se:bidiEnabled", false);
// Bidding-screen suite: assert on the reply at session start, not at first
// use. se:bidiEnabled false is the DEFAULT for a session that never asked,
// so the check that means something is the address - we did ask.
Capabilities returned = driver.getCapabilities();
if (!(returned.getCapability("webSocketUrl") instanceof String)) {
throw new IllegalStateException(
"asked for the bidirectional channel; reply has no webSocketUrl and "
+ "se:bidiEnabled="
+ returned.getCapability("se:bidiEnabled"));
}go deeper
Know that a session can be created successfully while a capability you asked for was quietly declined. Record which keys came back, once per session — the key names rather than the raw values, because a returned address can carry a password.
Be ready to describe both paths: a rewritten address when the channel exists, and a filtered key plus a false flag when it does not. Both live in the same reply — and the false flag is also what a session that never asked for the channel gets.
Recognise the signature — creation succeeds, the failure lands far later, and the client's message blames the request. Diagnose by reading the reply against the request, then separate a client that never asked in a recognised form from a Node that cannot and a Grid that will not.
Decide whether optional channels may be silently absent in your estate. If a suite depends on one, the harness should assert it at session start, so a deployment change surfaces as a clear failure rather than as scattered flakiness.
## An outcome written down, not an error The instinctive model is that asking for something the far side cannot provide produces an error at session creation. On a grid it does not. Selenium Grid gives you the session and records the outcome in the capabilities it returns. Nothing throws, nothing warns on the client, and the run proceeds — until the first line of code that needs the channel. For a suite driving a live-auction bidding screen, that gap can be long: a test that opens the session, logs in, seeds a few lots and only then subscribes to network events fails long after the decision that doomed it. ## What Selenium Grid writes on each path Inside `LocalNode`, `createExternalSession` builds the capability map the client will see. For the bidirectional channel it weighs three things: whether the Node supports it, whether the Grid has it enabled, and whether the **merged** capabilities carry `webSocketUrl` as a string. | outcome | what appears in the reply | |---|---| | channel available | `webSocketUrl` rewritten onto the Grid, browser socket kept in `se:gridWebSocketUrl` | | channel absent — declined, or never asked for | every `webSocketUrl` key filtered out, `se:bidiEnabled` set to `false` | | DevTools bridge absent | every `se:cdp` key filtered out, `se:cdpEnabled` set to `false` | The filtering is literal: the Node copies across every entry whose key does not start with `webSocketUrl`, then stamps the boolean on the result. The reply carries the outcome immediately, in two forms — an **absence** and a **flag**. But that negative branch is a single `else`, and it is wider than a refusal. The Node computes support as *the Node can* **and** *the merged `webSocketUrl` is a String*; an absent capability is `null`, which is not a String. So a plain session that never asked goes down the same branch as one that was turned away, and comes back stamped `se:bidiEnabled: false`. **On its own the flag is the default, not a verdict.** It is evidence only once you know your request carried the capability — and Selenium's comment names a third way in: a browser that answers `webSocketUrl` with a boolean rather than an address. ## Why the client's error blames the wrong thing Selenium's `RemoteWebDriver` builds its bidirectional connection at the end of session start. It reads `webSocketUrl` out of the capabilities it just received and: - if the value is not a string, it returns an empty optional and moves on — with **no log line at any level**; - if the string will not parse as a URI, it logs a warning and returns empty; - if the scheme is neither `ws` nor `wss`, it logs a warning and returns empty; - if the socket itself will not open, it closes the client and returns empty. None of those paths throws. The exception arrives later, when something asks for the handle and the optional is empty, and its message advises checking whether the browser supports the feature and whether the request capability was set. That points at your request — which was fine. The decision was the intermediary's, written into the reply. That is the whole signature, and why this is a senior question: **the error message names the request, the evidence is in the reply, and the two are separated by the entire run.** ## How to diagnose it quickly 1. Record which keys came back at session start, every run. The key names are the diagnosis; log an address only masked, because the Grid's rewrite copies the public grid URL's user info into every address it builds. 2. Read the flag against your own request. On its own a false channel flag only says there is no channel — it is what an ordinary session that never asked gets. It means a refusal only if your request carried the capability and it still came back false. 3. Check whether an address that *did* come back carries a WebSocket scheme. A refusal and a malformed address land in the same empty optional from different causes. 4. Separate the three ways in: the client never asked in a form the Node recognises, the Node cannot, or the Grid will not. All three produce the same reply; only the request and the deployment say which. 5. If you need the channel, assert on the reply at session start and fail there, not far later on a misleading exception. ## The general rule this is a case of An intermediary that rewrites addresses into a reply is also the thing that can leave one out. Both outcomes are expressed in the same place, and neither as a protocol error: - A **rewritten** address means the channel exists and the intermediary will proxy it. - A **missing** address plus a negative flag means the channel does not exist and the session is otherwise fine — but "does not exist" and "was declined" are the same reply, separated only by your request. On a hosted provider you cannot read the code that chose, but the shape is identical: a provider can hand you a working session while quietly declining a capability you asked for. Asking for something is not the same as having it, and the reply is where the difference is recorded — checked as the session is created, for the price of a single assertion.
- If the reply's socket address is rewritten onto the Grid, how does the browser's own socket get used?Selenium Grid keeps it. On the positive path `LocalNode` stores the browser's original socket address in `se:gridWebSocketUrl` and returns a Grid-owned address instead. When the client connects to that public path, `ProxyNodeWebsockets` reads the kept value back out of the capabilities and opens the upstream socket itself, relaying messages in both directions.
- What would make Selenium's client refuse a returned address that is present?Shape. The client accepts the value only if it is a string, parses as a URI, and carries a `ws` or `wss` scheme. Of the four ways it can give up, three log a warning — an unparseable URI, a wrong scheme, a socket that will not open — and the first does not: a value that is present but not a String returns an empty optional with no log line at any level, which is exactly the branch a boolean `webSocketUrl` takes. So a present, unusable address can leave no trace on the client at all, which is why the flag is worth reading separately from the address.
- How should a suite that genuinely requires the channel be written?Fail fast on the reply. Read the returned capabilities immediately after the session is created, check the address and the flag together, and abort with a message naming what you asked for and what came back — the flag alone cannot tell a refusal from a request you never made. That turns a late, misleading exception into an accurate one raised at the point of the actual decision, and it costs a single assertion.
saying these in an interview costs you the question
- Expecting a declined channel to fail session creation
- Trusting the client's exception message to name the real cause
- Reading a false channel flag as a refusal without checking what you asked for
- Assuming the address is absent because the network dropped it
- Expecting a client warning whenever a returned address is unusable
- Believing the browser alone decides whether the channel exists