skip to content

Why must a WebDriver session id from a routed grid be treated as opaque?

level: juniorimportance: nice to knowfreq 50%

answer

  1. the spec says UUID, but permits more
  2. a proxy has to remember the upstream
  3. the cheapest place to remember it
  4. routing state riding in the identifier
  5. send back exactly what you were given

basics

~20 s

A router in front of the browsers can rewrite the identifier before returning it, packing its routing state into the string. What you hold is longer than the upstream id, and only a router with the same table reads it.

solid answer

~50 s

The W3C WebDriver specification calls a session ID the string representation of a UUID, but it also lets an **intermediary node** return data merely *isomorphic* to an endpoint's, and notes that such a node typically has to track the upstream `sessionId` and URL somehow. Ggr — unmaintained by its own README — does exactly that in the reply: it concatenates an md5 of the host it chose onto the front of the id that host issued, and returns the longer string. Every later command arrives with that prefix in the path, and Ggr slices it off, looks the host up in an in-memory routes map and forwards the rest. So the value your client holds is longer than the browser's own, is no longer UUID-shaped, and carries routing state that belongs to the router. Send it back unchanged; never parse it, length-check it or rebuild an address from it.

code

go · 14 lines
go
// Ggr (unmaintained per its own README): the new-session reply for the
// bidding-screen suite gets the chosen host's digest put in front of it.
resp["sessionId"] = h.Sum() + sess   // h.Sum() is md5 of scheme://host:port

// ...and every later command is demultiplexed by that same prefix.
sum := r.URL.Path[head:tail]         // md5SumLength == 32
proxyPath := r.URL.Path[:head] + r.URL.Path[tail:]

h, ok := routes[sum]
if ok {
    r.Host = h.Net()
    r.URL.Host = h.Net()
    r.URL.Path = proxyPath           // the hub never sees the prefix
}

go deeper

for a junior

Store the session id as a plain string and send it back exactly as received. Do not check its length or shape, and do not slice pieces out of it for names or keys.

for a middle

Be able to say why a router would rewrite the id at all, and what it does with the prefix on the next request. The point is that a proxy must remember the upstream session somewhere.

for a senior

Watch for harness code that infers structure from the id — validators, log truncation, artefact naming. Those break silently the day a routing layer is added in front of an existing grid.

for a principal

Set the boundary rule once for the estate: identifiers issued by an external system are opaque tokens. Correlation belongs in your own ids, carried alongside, so a change of topology cannot break reporting.

## What a session id is supposed to be The WebDriver specification says a session has a **session ID**, which is the string representation of a UUID used to uniquely identify the session, set when the session is created. Read on its own, that sounds like a promise about the shape of the string, and clients are sometimes written as if it were one — validating the value, splitting it, or deriving something from it. The same specification then loosens it. It defines an **intermediary node** as a remote end that proxies, implementing both halves of the protocol, and it allows such a node to return success with data that is merely *isomorphic* to what a real remote end would have returned. It even says out loud how the bookkeeping is usually done: the intermediary must retain a reference to the session created upstream, and typically the `sessionId` and the URL and URL prefix of the upstream remote end will need to be tracked. ## Ggr tracks it inside the id itself Ggr, Aerokube's Selenium router — unmaintained by its own README, and read here only as a legible model of the mechanism — takes the cheapest possible reading of that sentence: it puts the routing state **into the identifier**. - When a hub creates the session, Ggr computes an md5 of that host's route, meaning its scheme, host and port joined together. - It prefixes that hex digest onto the id the hub issued and returns the concatenation to the client. - On every later command it slices the prefix back out of the request path, looks it up in a routes map built at startup, rewrites the request's host to the matching hub and forwards the remainder of the path. - The upstream hub never sees the prefix, so it keeps working with the id it issued. Ggr's own documentation explains why it is built this way: the router is deliberately stateless, so any instance behind a load balancer can serve any request for any session, because everything needed to route is in the string the client is already sending back. ## What that means for the client The id you hold is a different string from the one the browser's own remote end knows about. Concretely: | assumption | what actually holds | |---|---| | it is a UUID | it is a digest concatenated with a UUID | | it is fixed-length | its length depends on the hop count | | it names the session | it names the session *and* the route to it | | any hop can read it | only a hop with the same routing table can | For a suite driving a live-auction bidding screen, none of that matters as long as the client does the one correct thing: send back exactly the string it was given, on every request, unchanged. Trouble starts only when code tries to be clever with it. ## The habits that break 1. **Validating the id as a UUID.** A harness that rejects anything that is not UUID-shaped will refuse perfectly good sessions the moment a router is put in front of the grid. 2. **Parsing the id to find the host.** The prefix is a digest, not a hostname; it is meaningful only against the router's own map. Ggr exposes a separate lookup path for the rare case where you genuinely need the real hostname behind a session. 3. **Reusing the id across endpoints.** Sending it to a different router, or straight to a hub, produces a request nobody can resolve, because the prefix means nothing there. 4. **Building filenames or dashboard keys from a slice of it.** The slice you take may be routing state on one deployment and part of the real id on another. 5. **Logging only part of it.** A truncated id in a log is no longer usable for correlating with the far side. ## Why the rule generalises The routed grid is just the case where you can read the source. A hosted provider is an intermediary node too, and it is free to return an identifier of any shape that its own later requests can resolve — that is precisely what the specification permits it to do. You cannot inspect its scheme, and you do not need to: the contract you were given is *return this string as it was handed to you*. So the safe posture is the same everywhere. Treat a session id as an **opaque token**: store it as a string, pass it through untouched, log it whole, and never let a test or a harness infer anything from its length, its characters or its structure. Every property beyond "it identifies my session to the endpoint that issued it" is an accident of a particular deployment, and the next hop you add can change it.

  • If the id carries the route, how does a second router instance serve the same session?
    By holding the same table. Ggr builds its routes map at startup from its quota files, keying each host by the digest of its route, so any instance with identical files resolves the same prefix to the same host. That is what makes the router stateless: nothing about the session is remembered locally, because everything needed is in the string the client keeps sending back.
  • How would you find the real host behind a session without parsing the id?
    Ask the router. Ggr exposes a host-lookup path that takes the full session id and answers with the name, port and configured weight of the host actually running it. That keeps the id opaque to your code while still letting an operator resolve it, which is exactly the split you want: the client passes the token through, the tooling asks the component that issued it.

saying these in an interview costs you the question

  • Validating a session id against a UUID pattern
  • Splitting a session id to recover the host behind it
  • Assuming every hop sees the same session identifier
  • Reusing a session id against a different endpoint
  • Deriving filenames or report keys from part of the id