skip to content

Your Go login handler sends Set-Cookie but the next request has no session cookie — how do you diagnose it?

level: seniorimportance: should knowfreq 46%

answer

  1. read the raw header before anything else
  2. rejected differs from stored but unsent
  3. check Domain, Path, Secure, SameSite
  4. Go never adds Secure for you
  5. an invalid Domain is dropped, not refused

basics

~20 s

First decide whether the browser rejected the cookie or stored it and declined to send it, by comparing the raw Set-Cookie line with the browser's cookie store. Then check the Go fields that produce each case: Domain, Path, Secure and SameSite.

solid answer

~50 s

Split the problem in two before touching code. Read the raw `Set-Cookie` line off the login response, then open the browser's cookie inspector. **Not in the store** means the browser rejected the header: a `Domain` that is not a suffix of the page's host, `SameSiteNoneMode` sent without `Secure` (Go writes `SameSite=None` verbatim and never adds `Secure` for you), or a `Secure` cookie over plain http. **In the store but not sent** means a scope mismatch: a `Path` narrower than the request path, a host-only cookie on a different subdomain, or `Secure` on an http request. Three Go fields fail quietly here — an invalid `Domain` is dropped from the header with a log line rather than rejected, an empty `Path` emits no `Path` attribute, and `SameSite`'s zero value emits no attribute at all. Asserting `(*http.Cookie).String()` in a test catches all three.

code

go · 12 lines
go
c := &http.Cookie{
	Name:     "session",
	Value:    sid,
	Domain:   "https://app.example.com", // not a domain
	Path:     "/",
	Secure:   true,
	SameSite: http.SameSiteNoneMode,
}
if err := c.Valid(); err != nil {
	log.Print(err) // http: invalid Cookie.Domain
}
http.SetCookie(w, c) // writes the cookie WITHOUT any Domain attribute

go deeper

for a junior

Learn to look at the actual Set-Cookie line and the browser's cookie list before changing code, and to set Path explicitly rather than leaving the field empty.

for a middle

Explain what each of Domain, Path, Secure and SameSite does to whether a cookie is stored and whether it is attached, and what net/http emits for the zero value of each.

for a senior

Show the split-in-half method — rejected versus stored-but-unsent — name the fields that fail quietly in Go, and lock the emitted scope into a handler test so it cannot drift.

for a principal

Own the cookie scope as a cross-service decision: whether sessions are host-only or shared across subdomains, and what that commits every future service on the domain to.

## Cut the problem in half first "The cookie isn't coming back" is two completely different bugs with the same symptom, and the fix for one is never the fix for the other: 1. **The browser never stored it.** The `Set-Cookie` header arrived and was rejected. 2. **The browser stored it but did not attach it** to the request you are looking at. One comparison separates them. Capture the raw response header from the login endpoint — `curl -i` against the endpoint, or the network panel's raw view, not the prettified cookie table — and then look at the browser's cookie inspector for that site. If the cookie is absent from the store, you are in case 1. If it is present with attributes you can read, you are in case 2 and the question becomes "why doesn't this request match those attributes?" This matters because the classic report — "users get logged out, but only on one browser" — points at case 1 with a policy difference, while "logged out only on /api paths" points at case 2 with a `Path` problem. ## Case 1: rejected on arrival The usual causes, each traceable to one field of `http.Cookie`: - **`Domain` does not match the page's host.** A cookie may only be scoped to the host that set it or a parent domain. Setting `Domain: "example.com"` from a response served by `other.test` is discarded outright. - **`Domain` is not a domain at all.** `Domain: "https://app.example.com/"` — a URL pasted into the field — is the Go-specific version. `net/http` does not reject the cookie: it **drops the `Domain` attribute**, logs a line about an invalid `Cookie.Domain`, and writes everything else. The result is a host-only cookie that works on `app.example.com` and vanishes on every other subdomain, which is exactly the "broken for some users" shape. - **`SameSiteNoneMode` without `Secure`.** Go writes `SameSite=None` because you asked for it and does **not** add `Secure`. Browsers reject that combination, so the cookie is dropped on arrival. If you need a cookie on cross-site requests you must set `Secure: true` yourself and serve over https. - **`Secure: true` over plain http.** Common in local development, where the app runs on `http://` and the production cookie settings were copied over. ## Case 2: stored but not attached - **`Path`.** A stored cookie is sent only when the request path is within its path scope. If `Path` was left empty, Go emits no `Path` attribute and the browser derives one from the URL that set the cookie — so a cookie issued by `POST /api/auth/login` is scoped to `/api/auth/` and never reaches `/dashboard`. Set `Path: "/"` deliberately. - **Host-only versus domain scope.** With `Domain` empty the cookie is host-only: issued by `app.example.com`, never sent to `api.example.com`. Setting `Domain: "example.com"` widens it to the whole family, which is a deliberate choice, not a default. - **`SameSite`'s zero value.** `SameSiteDefaultMode` makes Go emit no `SameSite` attribute, which leaves the decision to the browser's own default. Two browsers with different defaults then behave differently on a cross-site navigation or a cross-origin `fetch` — the single most common explanation for "it only breaks in one browser". Set the mode explicitly so behaviour is the same everywhere. - **The `fetch` on the client did not ask for credentials.** Nothing to do with Go, but worth eliminating before rewriting handler code. ## Prove what Go actually emitted Do not reason about the struct; look at the bytes. `(*http.Cookie).String()` renders exactly the header value `http.SetCookie` will add, so: ```go rec := httptest.NewRecorder() login(rec, httptest.NewRequest("POST", "/api/auth/login", nil)) for _, c := range rec.Result().Cookies() { t.Log(c.Name, c.Path, c.Domain, c.Secure, c.SameSite) } ``` A test like that pins the scope of the session cookie forever and catches the day someone "tidies up" the `Path` field. Add a `(*http.Cookie).Valid()` call in the issuing helper so a malformed `Domain` becomes a startup-time or request-time error instead of a dropped attribute and a log line nobody reads. ## The ordering trap worth ruling out early If there is no `Set-Cookie` line at all, none of the above applies. Two Go-specific causes: `http.SetCookie` was called after the handler already wrote the body or the status, so the header map was already committed; or the cookie's `Name` is not a valid token, in which case `String()` returns empty and `SetCookie` adds nothing. Both are silent. Checking the raw response first is what surfaces them in seconds instead of an afternoon.

  • The cookie is in the browser's store but never reaches /dashboard. What is the likely field?
    `Path`. If it was left empty, Go emits no `Path` attribute and the browser scopes the cookie to the directory of the URL that set it — `/api/auth/` for a login at `/api/auth/login` — so nothing outside that prefix gets it. Set `Path: "/"` explicitly on any cookie the whole site needs.
  • What does the zero value of http.Cookie.SameSite emit, and why does that cause browser-specific behaviour?
    `SameSiteDefaultMode` is the zero value and makes Go emit **no** `SameSite` attribute at all. The browser then applies its own default, and those defaults differ between browsers and versions, so the same deployment behaves differently per client. Setting `SameSiteLaxMode` or `SameSiteStrictMode` explicitly removes the variability.
  • You set SameSite to http.SameSiteNoneMode and the browser discards the cookie entirely. Why?
    Go writes `SameSite=None` exactly as asked and does not add anything else. Browsers reject `SameSite=None` unless `Secure` is also present, so the header arrives and is dropped. Set `Secure: true` on the same struct and serve the response over https; `net/http` will not do it for you.
  • There is no Set-Cookie line on the response at all. What two Go-side causes do you check?
    Either `http.SetCookie` ran after the handler already wrote the status or body, so the header map was committed and the addition was ignored; or the cookie `Name` is not a valid HTTP token, which makes `(*http.Cookie).String()` return empty and `SetCookie` add nothing. Both are silent — no error, no panic.

saying these in an interview costs you the question

  • Assumes net/http adds Secure when SameSite is None
  • Thinks an invalid Domain makes SetCookie fail loudly
  • Puts a full URL, with scheme, in the Domain field
  • Never checks whether the browser stored the cookie at all
  • Relies on the SameSite zero value being Lax
  • Debugs from the struct instead of the raw header