Selenium's addCookie returned without throwing, yet getCookieNamed comes back null — what explains that?
answer
- No exception is not proof of storage
- The browser makes the final decision
- Read it back on the very next line
- Check expiry units and the path
- Secure flag against a plain-http page
basics
~20 sThe driver handed the cookie to the browser, and the browser declined to keep it or stored it outside the current page's scope. An expiry in the past, a Secure flag on a non-secure page, or a path the page does not match all read back as nothing.
solid answer
~40 s`addCookie` succeeds when the remote end accepts the command; the browser then applies its own storage rules, and a cookie it drops leaves no error to catch. Four causes cover almost every case on the shift-rota console. An `expiresOn` already in the past — usually a millisecond arithmetic slip — is stored and immediately expired. `isSecure(true)` while the page is on a non-secure origin means the browser refuses it. A `path("/reports")` set while the document is at `/wards/icu` is stored but out of scope, so the read misses it. And `sameSite("None")` without `isSecure(true)` is rejected by the browser's own rule. Read the cookie back immediately after adding it, then strip attributes one at a time until it survives. This assumes Selenium 4, where `Cookie` carries `sameSite`.
code
java · 13 linesDate expiry = Date.from(Instant.now().plus(Duration.ofHours(12)));
Cookie shiftView = new Cookie.Builder("rota_view", "week")
.domain("rota.stmartins.test")
.path("/")
.expiresOn(expiry)
.isSecure(true)
.isHttpOnly(false)
.sameSite("Lax")
.build();
driver.manage().addCookie(shiftView);
assertNotNull(driver.manage().getCookieNamed("rota_view"));go deeper
Take away one habit: after adding a cookie, read it back with getCookieNamed and assert it is there. A silent add tells you nothing on its own.
Explain that the driver only requests storage and the browser decides, then name the attributes that make a cookie disappear: an expiry in the past, Secure on a non-secure page, a narrow path.
Work the diagnosis in order — read back, dump the set, compare attributes with the current URL, strip attributes one at a time — and distinguish a silent rejection from the loud domain exception.
Set the convention that stops the class of bug: a seeding helper that always asserts the read-back, seeds at the site root, and computes lifetimes in units that cannot be misread.
## Why a rejected cookie can be silent The cookie API is a request to the browser, not a guarantee. Selenium sends the add-cookie command; the remote end hands the cookie to the browser's store; the browser applies its own rules about what it is willing to keep. The protocol does define an **unable to set cookie** error — `UnableToSetCookieException` in the Java client — but a cookie quietly declined on storage rules usually does not surface as one. The command reports success and the next read simply finds nothing. That is why the discipline for seeding a cookie is always the same: **add it, then read it back in the same breath**. A one-line assertion after `addCookie` turns a mystery failure three steps later into a failure on the line that caused it. ## The four causes worth memorising | Symptom | Cause | Check | |---|---|---| | Never appears, any page | `expiresOn` in the past | print `expiry.getTime()` against `System.currentTimeMillis()` | | Never appears on `http://` | `isSecure(true)` on a non-secure origin | compare the flag against `driver.getCurrentUrl()` | | Appears on one page, not another | `path` narrower than the current URL | compare `getPath()` against the path under test | | Never appears, cross-site cookie | `sameSite("None")` without `isSecure(true)` | set both together or neither | ## Expiry: the arithmetic that bites `Cookie.Builder.expiresOn(Date)` takes a `java.util.Date`, and the wire format carries expiry as **whole seconds since the epoch**, so sub-second precision is lost on the way out. Two slips are common: - Adding a value in **seconds** to a millisecond clock — `new Date(System.currentTimeMillis() + 3600)` is one hour in intent and 3.6 seconds in fact, so the cookie is very likely dead before the next assertion. - Reusing a `Date` computed at class-load time in a suite that runs for an hour, so late tests seed a cookie that has already expired. Prefer `Date.from(Instant.now().plus(Duration.ofHours(12)))`, which cannot be read as the wrong unit. And remember that omitting `expiresOn` entirely is legitimate: the result is a **session cookie**, which lives as long as the browser does and is usually exactly what a test wants. ## Path, and the page you happen to be on A cookie's `path` decides which URLs it is associated with. Seeding `path("/reports")` while the browser is on `https://rota.stmartins.test/wards/icu` stores a perfectly good cookie that the current document simply does not match, so `getCookieNamed` returns `null` and the console never sends it on that page. The symptom is distinctive: the cookie "appears" once the test navigates to the reporting section, and vanishes again on the way back. Three habits remove the whole class of puzzle: - Seed with `path("/")` unless the test is specifically about path scoping; it is the widest scope within the origin. - Read the cookie back from the same URL you seeded it on, so a scope mismatch cannot masquerade as a rejection. - Compare `getPath()` on a cookie that *is* returned with the path you asked for, because the browser can normalise it. ## Secure and SameSite `isSecure(true)` marks the cookie as one the browser will only keep and send over a secure connection. Seed it while the page under test is served over plain `http://` and the browser declines it. `sameSite("None")` carries a matching requirement from the browser's side — it has to be paired with the Secure flag. Selenium's job here is narrow: `Cookie.Builder` carries the attributes to the remote end and `getSameSite()` reads back what was stored. The accept-or-reject decision is the browser's, and Selenium has no channel to explain it to you. ## Diagnosing it in order 1. Assert immediately: `assertNotNull(driver.manage().getCookieNamed("rota_view"))` on the line after `addCookie`. 2. Print the whole set with `getCookies()` — a cookie present with different attributes is a different bug from a cookie that is absent. 3. Compare the cookie's `path` with `driver.getCurrentUrl()`, and its `isSecure()` with the URL's scheme. 4. Rebuild it with the two-argument `new Cookie(name, value)` constructor. If the bare cookie survives, add attributes back one at a time; the one that makes it disappear is your answer. 5. Only then look at the application. A cookie the browser accepted and the console ignores is a value problem, not a storage problem. A cookie for the **wrong domain** throws `InvalidCookieDomainException` — loud, immediate, easy. A cookie the browser declines on its own storage rules is silent. Knowing which of the two you are looking at is most of the diagnosis, and it is the distinction interviewers are listening for.
- How do you tell a cookie the browser refused from a cookie that is merely out of scope?Navigate to the widest URL on the origin — the site root — and read again. A cookie stored with a narrow path reappears there; a cookie the browser never kept is still missing. Printing the whole `getCookies()` set at the root is the fastest single check, because it shows attributes as stored rather than as requested.
- Two cookies share a name at different paths. What does getCookieNamed return?Whichever of the matching cookies the returned set yields first — the set is unordered, so the choice is not one you should depend on. The delete-by-name call has the same blind spot: it takes only a name, with no path to disambiguate. If a test needs one specific cookie, read the whole set and filter on `getPath()` yourself.
saying these in an interview costs you the question
- Assumes no exception means the cookie was stored
- Computes expiry by adding seconds to a millisecond clock
- Seeds a Secure cookie against a plain-http page
- Sets a narrow path and reads from another page
- Retries the add in a loop instead of reading it back