skip to content

In the W3C WebDriver spec, why is an intermediary node's own extension capability absent from what the browser receives?

level: seniorimportance: must knowfreq 58%

answer

  1. the spec says must not be forwarded
  2. consumed, not ignored
  3. only its own namespace is removed
  4. three places a capability can hide
  5. absence downstream proves nothing

basics

~20 s

The specification forbids it. An intermediary node may define extension capabilities to help it route a new session, but those specific capabilities must not be forwarded to the endpoint node, so the browser never sees them.

solid answer

~40 s

The spec's `New Session` section says an intermediary node is free to define extension capabilities to assist in routing, *however, these specific capabilities **must not be forwarded** to the endpoint node*. The rule is narrow and deliberate: only the intermediary's **own** namespace is removed. Everything else — the standard keys, and a browser vendor's own options map — is forwarded untouched, because those are addressed to the hop that actually owns the browser. Selenoid — unmaintained, per its own README — implements exactly this in `removeSelenoidOptions`, which deletes `selenoid:options` from `desiredCapabilities`, from `alwaysMatch` and from every `firstMatch` entry before proxying onward; its test `TestSessionCreatedRemoveExtensionCapabilities` asserts all three are gone while `goog:chromeOptions` is still present. Operationally: a setting aimed at the service is consumed by the service, and its absence downstream is the specification working.

code

go · 19 lines
go
// Selenoid (unmaintained, per its own README) - selenoid.go, trimmed
func removeSelenoidOptions(input []byte) []byte {
	body := make(map[string]interface{})
	_ = json.Unmarshal(input, &body)
	const selenoidOptions = "selenoid:options"
	if raw, ok := body["capabilities"]; ok {
		if c, ok := raw.(map[string]interface{}); ok {
			if raw, ok := c["alwaysMatch"]; ok {
				if am, ok := raw.(map[string]interface{}); ok {
					delete(am, selenoidOptions)
				}
			}
			// the same delete runs for every capabilities.firstMatch
			// entry, and for the legacy desiredCapabilities object
		}
	}
	ret, _ := json.Marshal(body)
	return ret
}

go deeper

for a junior

Know that settings aimed at the service in front of the browser are used up by that service. Not finding them further down is expected, not a defect worth reporting.

for a middle

Be ready to quote the rule as a prohibition on forwarding an intermediary's own extension capabilities, and to explain why a vendor's engine options travel on while the routing namespace does not.

for a senior

Be ready to debug this end to end: check the effect rather than the returned capabilities, remember that a mistyped namespace is forwarded rather than refused, and know that stripping must cover every position a capability can occupy.

for a principal

Be ready to reason about what a namespace should be allowed to carry at all, given that whatever sits in it stops at the first hop that owns it and is therefore invisible to everything downstream that might otherwise audit it.

## The rule, in the specification's own words The `New Session` command section of the W3C WebDriver specification says two things in consecutive sentences. First, that if the remote end is an **intermediary node** it may use the result of capabilities processing to route the request to the appropriate **endpoint node**. Then: > An intermediary node is free to define extension capabilities to assist in this process, however, these specific capabilities must not be forwarded to the endpoint node. That is a hard prohibition, and it is what makes a namespaced setting aimed at a service behave differently from every other key in the request. ## Why the rule has to exist An endpoint node is a browser driver. Run it through the spec's validation algorithm and an unrecognised **bare** key is an `invalid argument` error, while an unrecognised **namespaced** key is carried through unchanged. Neither outcome is what you want for a routing setting: - If the intermediary forwarded its own namespace, the driver would carry meaningless data into the capabilities it validates, and might or might not echo it back: the spec makes adding extension capabilities to the reply optional and lets their values be elided. - The setting has already done its job by the time routing is decided. There is nothing downstream that could act on it. - Worst, a namespace can carry account information. The spec's own worked example puts a user and a password inside one, and forwarding those to a browser process would push a credential a hop further than it needed to go. So the intermediary consumes what is addressed to it and hands on what is not. ## The rule is narrow — read the word "specific" The prohibition covers the extension capabilities *that intermediary defined*, not extension capabilities in general. A request typically carries several namespaces at once, aimed at different hops: | what is in the request | who consumes it | forwarded to the browser? | |---|---|---| | standard keys such as `browserName` | the endpoint node | yes | | the intermediary's own namespace | the intermediary | **no** | | a browser vendor's options map | the browser driver | yes | | a namespace nobody in the path owns | nobody | yes, unchanged | The last row is the one people get wrong. An intermediary does not tidy up. It removes its own keys and leaves everything else exactly as it found it, including namespaces it has never heard of. ## An open implementation you can read Selenoid — unmaintained, per its own README — is a session-container service that acts as an intermediary node, and its handling is a single function. `removeSelenoidOptions` unmarshals the request body and deletes the constant `"selenoid:options"` from three places: 1. the legacy `desiredCapabilities` object, if present; 2. `capabilities.alwaysMatch`; 3. every entry of the `capabilities.firstMatch` list. Then it re-marshals and proxies onward. Its test, `TestSessionCreatedRemoveExtensionCapabilities`, stands a stub browser endpoint behind it, posts a body carrying `selenoid:options` in all three positions plus `goog:chromeOptions` in `alwaysMatch`, and asserts that the stub saw none of the three `selenoid:options` occurrences and did see `goog:chromeOptions`. That single assertion pair is the whole rule, executable. Note the three positions. Deleting from `alwaysMatch` alone would leave a copy in a `firstMatch` entry, and the merge step would put it right back. A correct implementation strips every place a capability can legally sit. ## What it means when you are debugging A recurring support conversation on a school-timetabling suite goes like this: someone captures the session's returned capabilities, cannot find the settings they sent to the service, and files a bug saying the settings were ignored. They were not ignored — they were consumed. The diagnostic order that actually works: - Do not treat absence downstream as evidence of anything. Absence is mandated. - Check the observable effect instead: did the thing the setting asked for actually happen for this session? - Check the spelling of the namespace, since an unrecognised prefix is forwarded rather than rejected and therefore looks identical from the client. - Check where in the request the map was placed. A hop that strips only `alwaysMatch` and a client that emits only `firstMatch` will disagree. ## The one asymmetry worth memorising The specification carries two forwarding rules and they point in opposite directions. An extension capability defined by an intermediary **must not** be forwarded. A custom top-level parameter — data sitting beside `capabilities` in the request body rather than inside it — **must** be forwarded to subsequent remote end nodes. Same request, same intermediary, opposite obligations, decided purely by which part of the body the data sits in. That is not a quirk. A capability describes what the session should be, so a hop that understands one is expected to act on it and remove it. A top-level parameter is an argument to the command itself, and a chain of hops may all need it. Knowing which half of the body your data is in tells you whether it stops at the first hop that understands it or travels the whole way down.

  • Why must the stripping cover firstMatch entries and not only alwaysMatch?
    Because capabilities processing merges `alwaysMatch` with each `firstMatch` entry before matching. A copy left in a `firstMatch` entry survives the merge and reappears in the capabilities handed to the endpoint node, defeating the deletion. Selenoid — unmaintained, per its own README — therefore has `removeSelenoidOptions` walk every `firstMatch` entry as well as `alwaysMatch`, and its test asserts all positions are clear rather than just the first.
  • Does the same rule apply to a browser vendor's options map passing through the intermediary?
    No. The prohibition names the extension capabilities *that intermediary defined*, so a vendor's engine options are forwarded untouched — they are addressed to the endpoint node, not to the hop in front of it. The test in Selenoid (unmaintained, per its own README) makes the distinction explicit by asserting `goog:chromeOptions` is still present in what the stub browser endpoint received.
  • How would you prove a setting aimed at the service actually took effect?
    By checking the effect, never the request. Assert the observable outcome the setting asks for — a viewport dimension, a capture artefact appearing, a labelled session in the service's own listing — inside the test or immediately after it. The returned capabilities are the wrong evidence, because the spec requires the intermediary's namespace to be absent from them whether the setting worked or not.

A courier hands over the parcel but keeps the delivery docket. The docket told the courier which door to knock on, and it would mean nothing to the person who opens it.

saying these in an interview costs you the question

  • Reads absence downstream as the setting being ignored
  • Thinks an intermediary strips all namespaced keys, not only its own
  • Believes the browser driver sees the routing capabilities
  • Strips only alwaysMatch and forgets firstMatch entries
  • Assumes forwarding rules are the same for capabilities and top-level parameters