A Selenium Grid Node runs Chrome, yet requests pinning browserVersion never match its slot. Why, and what fixes it?
answer
- The browser is there, the promise is not
- Detection advertises less than you think
- Unpinned requests still land fine
- Version comparison runs in one direction
- An empty declared browserVersion never compares equal
basics
~20 sThe Node auto-detected Chrome, so its stereotype declares only a browser name and a platform, never a version. Selenium 4 treats an undeclared version as no match. Declare the version explicitly in a driver-configuration stereotype instead.
solid answer
~40 sDriver detection in Selenium 4 is deliberately thin: `ChromeDriverInfo.getCanonicalCapabilities()` returns only `browserName`, and the Node adds `platformName` for the host it runs on. The installed browser is never launched to read its version, so an auto-detected slot advertises no `browserVersion` at all. `DefaultSlotMatcher` compares versions through `SemanticVersionComparator`, and an empty declared version never compares equal, so every version-pinned request is refused while unpinned ones sail through. The fix is to declare a `[[node.driver-configuration]]` block whose stereotype carries the fully qualified `browserVersion`, set `detect-drivers = false` so the unversioned slot does not linger, pin the driver with `webdriver-executable`, and restart. Declare the full version, not the major alone: a short stereotype cannot satisfy a fully qualified request.
code
toml · 8 lines[node]
detect-drivers = false
[[node.driver-configuration]]
display-name = "Prescription queue - Chrome 131 pinned"
max-sessions = 4
webdriver-executable = "/opt/drivers/131/chromedriver"
stereotype = '{"browserName": "chrome", "browserVersion": "131.0.6778.85", "platformName": "linux"}'go deeper
Remember that a Node advertises only what its configuration says, and that having Chrome installed on the machine is not the same as offering a Chrome 131 slot to the Grid.
Explain what driver detection actually puts in a stereotype, and why a request that pins a browser version behaves completely differently from one that leaves the version out.
Show a diagnosis order: compare the stereotype in the Node's registration log with the failing request's capabilities, then decide between declaring the version and dropping the pin from the suite.
Decide where version pinning lives at all. Pinning on the Node makes the fleet the source of truth and forces coordinated upgrades; pinning in the suite pushes that coupling into every test repository.
## The symptom The prescription-queue suite pins `browserVersion` so that a dispensing screen is always exercised on the browser build the pharmacy actually deploys. The Node has Chrome installed, was started with driver detection on, registered cleanly, and is heartbeating. Requests that name only `browserName` land on it happily. Requests that also pin `browserVersion` never do. Nothing in the Node's own health tells you why. ## What an auto-detected slot actually advertises Detection is deliberately thin. `ChromeDriverInfo.getCanonicalCapabilities()` returns exactly one capability, `browserName: chrome`; `GeckoDriverInfo` returns exactly `browserName: firefox`. The Node then runs `NodeOptions.enhanceStereotype`, which stamps `platformName` from the running host and adds `se:` keys for VNC or managed downloads when those are on. **The installed browser's version is never read into the stereotype.** Selenium 4 does not launch the browser to interrogate it at configuration time, so an auto-detected slot advertises a browser name and a platform and nothing else. ## How browserVersion is actually compared `DefaultSlotMatcher` delegates to `SemanticVersionComparator`, which splits both strings on dots and walks the parts. Two asymmetries decide everything: - A part missing from the **request** inherits the stereotype's part at that position. - A part missing from the **stereotype** defaults to `0`. - An empty stereotype version sorts last and never compares equal. | Stereotype declares | Request asks for | Match? | |---|---|---| | `131.0.6778.85` | nothing at all | yes — the request states no preference | | `131.0.6778.85` | `stable` | yes — the literal string is treated as a wildcard | | `131.0.6778.85` | `131` | yes — the request's missing parts inherit the stereotype's | | `131` | `131.0.6778.85` | no — the stereotype's missing parts become `0`, so `0` faces `6778` | | nothing (auto-detected) | `131` | no — an empty declared version never compares equal | The last row is the bug. The fourth row is the one that bites teams who tried to fix it by declaring a bare major version. ## Why the Node still looks healthy Registration proves only that the Node reached the Distributor and is sending heartbeats. Routing is a separate, per-request decision taken against the stereotype the Node published. A perfectly healthy Node whose stereotype nobody asks for simply sits idle, so the fault is invisible from the Node's own liveness and shows up only as requests that never find a home. It is also why the failure survives the usual first reactions. Restarting the Node changes nothing, because it rebuilds exactly the same detected slot from the same configuration. Adding a second machine changes nothing, because the second one detects Chrome the same way. Only changing the declaration moves the outcome. ## The fix 1. Read the stereotype the Node logged when it registered, and compare it with the capabilities the failing request sends. Trust the log, not the config file. 2. Replace detection with an explicit `[[node.driver-configuration]]` block carrying `browserVersion` in the stereotype JSON. 3. Set `detect-drivers = false` in the `[node]` section, so the unversioned detected slot does not linger beside your declared one and quietly absorb unpinned traffic. 4. Declare the **fully qualified** version, not the major alone. A four-part declaration serves both a request for `131` and a request for `131.0.6778.85`; a bare `131` serves only the first. 5. Pin the matching driver binary with `webdriver-executable` in the same block, so the slot's promise and the process it launches cannot drift apart. 6. Restart the Node. Slots are built once from the configuration the process started with; editing the file changes nothing until then. ## The traps next door - **Vendor prefixes never route.** `goog:`, `moz:`, `ms:`, `safari:` and `se:` keys are skipped by the matcher. A stereotype that pins a beta binary through `goog:chromeOptions` still matches plain Chrome requests. - **Custom prefixed keys must be everywhere.** A key like `pharmacy:tier` is checked only against stereotypes that declare it; a Node silent on the key skips the check and matches anyway. Selenium's documentation is explicit that a custom capability has to be set on every Node and sent on every request. - **`platformName` is stamped, not blank.** Leaving it out of your config does not make the slot platform-agnostic; the Node fills in its own platform. - **Managed downloads are matched.** A request setting `se:downloadsEnabled` to true only matches a stereotype that also declares it true. - **An empty capability set matches nothing**, so a request that sends no capabilities at all is not a wildcard.
- The Node declares browserVersion 131 and the suite asks for 131.0.6778.85. Does that match?No. `SemanticVersionComparator` pads the stereotype's missing parts with zero while a missing part in the request inherits the stereotype's, so the comparison is asymmetric. `0` is then compared against `6778` and the versions differ. A short request against a long stereotype matches; a long request against a short one does not. Declare the fully qualified version on the Node.
- Only one of two Nodes declares a custom capability such as pharmacy:tier. Where does a request carrying it land?Either Node. `DefaultSlotMatcher` checks a custom prefixed key only against stereotypes that declare it; a stereotype silent on the key skips the check and matches anyway. Selenium's documentation is explicit that a custom capability must be set on every Node and included in every session request for the routing to be dependable.
- Why can a Node be registered and heartbeating while nothing ever routes to it?Registration proves only that the Node reached the Distributor and is alive. Routing is a separate per-request decision taken against the stereotype the Node published, so a healthy Node whose stereotype nobody asks for simply sits idle. Liveness and matchability are independent, which is why the fault is invisible from the Node's own health.
saying these in an interview costs you the question
- Assumes an auto-detected slot advertises the installed browser's version
- Thinks Grid falls back to any Chrome when the requested version is unavailable
- Blames the queue or the network before reading the Node's registered stereotype
- Treats browserVersion matching as symmetric between stereotype and request
- Believes editing the config file changes a running Node's declared slots