skip to content

A remote WebDriver endpoint address stops authenticating after the credit-union statements portal's account password is rotated to a value containing @ and : characters. What broke?

level: seniorimportance: should knowfreq 44%

answer

  1. the string is parsed before it is used
  2. the authority has a fixed grammar
  3. parsers disagree on a second at-sign
  4. the first colon ends the username
  5. user info wins over configured credentials

basics

~20 s

The address is parsed as a URL before anything reads the credential, so an unescaped at sign leaves parsers disagreeing about where the host starts, and a colon splits the user info early. Keep the credential outside the address.

solid answer

~50 s

Nothing authenticated because nothing got as far as authenticating. The endpoint string is parsed as a URL first, and the authority it parses is user info, then an at-sign, then host and port. A raw `@` inside the password adds a second at-sign, so Java reads a registry-based authority with no host and no user info at all; Selenium's `ClientConfig.baseUrl` accepts that happily, and the run instead dies on the new-session request, where `java.net.http.HttpRequest` rejects the address as an unsupported URI and quotes the rotated password in cleartext. A colon is subtler: Selenium's `PasswordAuthenticator` does `userInfo.split(":", 2)`, so a colon inside the password survives but a colon inside the username silently truncates it. The durable fix is to keep the credential out of the address altogether, using `ClientConfig.authenticateAs(new UsernameAndPassword(...))`, where the values are plain strings no URL parser touches.

code

java · 14 lines
java
// Credit-union statements portal suite, after a password rotation.
// The rotated value contains '@' and ':', so it must never be
// concatenated into the endpoint address.
URL endpoint = URI.create("http://grid.internal:4444/wd/hub").toURL();

ClientConfig config = ClientConfig.defaultConfig()
    .authenticateAs(new UsernameAndPassword(
        System.getenv("GRID_USER"),
        System.getenv("GRID_PASS")));

// RemoteWebDriver installs `endpoint` as the config's base URI.
// Because that URI carries no user info, JdkHttpClient uses the
// credentials above instead of parsing them out of the address.
WebDriver driver = new RemoteWebDriver(endpoint, new ChromeOptions(), config);

go deeper

for a junior

Be ready to say that an endpoint address is parsed as a URL, so characters like at-signs and slashes inside a password change what the address means before anyone checks the password.

for a middle

Be ready to name where the split happens: the at-sign separates user info from host, and the first colon separates username from password. Explain why percent-encoding is the minimum fix.

for a senior

Be ready to diagnose this under time pressure, recognising that an unsupported-URI error on the new-session request after a rotation points at your own string assembly, not at the far service, and to propose a design that removes the embedded form.

for a principal

Be ready to argue for a house rule that credentials never enter an address at all, and to say what enforcement looks like across many repositories when the embedded form is what most tutorials and vendor guides still show.

## The parse happens before the credential is ever read An endpoint address is a string, and every layer that receives it treats it as a URL before a credential. The authority has a fixed shape: an optional user-info component, an at-sign, the host, then an optional colon and port. That grammar is why the form works at all, and why the credential is fragile in a way a password field is not. When the credit-union statements portal suite is rotated onto a password full of reserved characters and pasted straight into the address, nothing has authenticated and failed — the parse produced a different address, or none. ## What each character does - **An unescaped `@`** adds a second at-sign, and parsers then disagree about where the host begins. Java's `java.net.URI` gives up on a server-based authority: host and user info both come back null. Python's `urllib.parse` and Node's WHATWG parser split at the **last** at-sign instead, recovering the intended host and the whole password. `curl` refuses the address as a bad hostname. Nothing treats your password's tail as a host; the hazard is that one string means three different things in three neighbouring tools. - **An unescaped `:`** is legal in the user info but is also the separator between username and password. Selenium's `PasswordAuthenticator` calls `userInfo.split(":", 2)`, so everything before the **first** colon is the username and the rest is the password. A colon inside the password is safe; a colon inside the username silently truncates it and pushes the remainder into the password. - **An unescaped `/`, `?` or `#`** ends the authority early, so host and port are gone before they are read; Java's `java.net.URL` refuses it outright with a `MalformedURLException`, inside your own assembly code. - **No colon at all** is not an error. Selenium's `PasswordAuthenticatorTest` asserts that a user-info string of just a name yields that name and an **empty** password — a silent, valid-looking wrong answer. ## Why the error names the wrong layer Selenium's `ClientConfig.baseUrl` does call `toURI()` and rethrow a `URISyntaxException` as a `RuntimeException`, so a construction-time failure feels likely. It is not what happens. Java accepts the doubled at-sign, so `RemoteWebDriver` builds its client without complaint and the address looks fine until it is used. The run dies on the new-session request: Selenium's `JdkHttpMessages` concatenates the base and `/session`, and `java.net.http.HttpRequest` rejects the result with `IllegalArgumentException: unsupported URI`, because a URI with no host cannot become an HTTP request. That `RuntimeException` path is real, but only for characters a URI forbids outright, such as a space or a brace. Two things follow. The message names a URI while you hunt an authentication problem, so the obvious hypotheses — the account was revoked, the service is down, the rotation did not propagate — all point at the far side while the fault is yours. And it quotes the whole address, so the rotated password is now in cleartext in a stack trace and in whatever collects it. Selenium masks credentials in the messages it formats itself; this one is formatted below that masking, by the JDK. ## The fix, and the trap inside the fix Two honest fixes exist, and they are not equivalent: 1. **Percent-encode the reserved characters** in the user-info component as you assemble the address. It works, but it puts a correctness requirement in whatever code concatenates the string, where encoding the wrong half is easy. 2. **Take the credential out of the address.** Selenium's client configuration accepts it as data: `ClientConfig.defaultConfig().authenticateAs(new UsernameAndPassword(user, pass))`. Those are plain strings no URL parser touches, so no character in a rotated password can change the meaning of the address. The trap is precedence. Selenium's `JdkHttpClient` reads `baseUri.getUserInfo()` first and, when it is non-empty, builds a `PasswordAuthenticator` from it; only when the user info is absent does it fall back to the credentials from `authenticateAs`. Because `RemoteWebDriver` always installs the URL argument as the base URI, a well-formed address that still carries a stale `user:password@` **silently wins** over the credentials you passed in as data. "We moved the credential out of the URL" is only true once the URL itself is clean. The doubled at-sign inverts even that: with no server-based authority there is no user info to read, so the configured credentials would have been used — had the request ever been built. ## Operating rules that survive a rotation - Assemble the address from parts at run time; never store a fully-formed address embedding a credential. - If the address must carry user info, percent-encode it at assembly, once, and never by hand. - Prefer the client's credential API, then assert the endpoint you pass alongside it has no user info. - Constrain generated passwords to URL-safe characters only if you cannot avoid the embedded form; that is a workaround, not a design. - When a rotation breaks a remote run, check the shape of the address before the account. The parse is upstream of everything else. ## What a weak answer sounds like A weak answer reaches for the service: the key must be wrong, the account disabled, the fleet unreachable. A strong answer notices that the symptom appeared the moment the *string* changed, and that a URL is a grammar, not a container. Every character that means something to a parser is a character that can break the run.

  • Where does a doubled at-sign in the address actually break the run?
    On the new-session request, not at client construction. Java accepts `http://user:pa@[email protected]:4444/wd/hub` as a registry-based authority with a null host, so Selenium's `ClientConfig.baseUrl` builds the client without complaint. The failure comes when Selenium's `JdkHttpMessages` appends `/session` and `java.net.http.HttpRequest` refuses the result with `IllegalArgumentException: unsupported URI`. `ClientConfig.baseUrl` does wrap a `URISyntaxException` in a `RuntimeException`, but only for characters a URI forbids outright, such as a space. Note what the failure prints: the whole address, password included.
  • If I pass credentials through authenticateAs and the address also has user info, which one is used?
    The address wins. Selenium's `JdkHttpClient` inspects `baseUri.getUserInfo()` first and builds a `PasswordAuthenticator` from it whenever it is non-empty; only when it is absent does it fall back to the credentials from `authenticateAs`. That makes a stale `user:password@` in a configured endpoint an invisible override, so it is worth asserting the endpoint has no user info.
  • How would you make this class of breakage impossible rather than merely rare?
    Stop producing the embedded form. Have one helper build the client from an address and a separate credential object, assert in that helper that the address carries no user info, and fail loudly if it does. Then a rotation can put any character it likes in the secret, because the secret never passes through a URL parser and no assembly code has to remember to encode it.

saying these in an interview costs you the question

  • Blames the account or the service before checking the address shape
  • Thinks reserved characters in a password are safe unescaped in a URL
  • Believes a colon in the username is equivalent to one in the password
  • Assumes authenticateAs overrides a credential already in the address
  • Treats percent-encoding by hand as a durable fix rather than a workaround
  • Expects a malformed address to fail at client construction rather than on the first request