In Go's net/http, how do you set a cookie on a response and read it back on the next request?
answer
- one call writes it, one call reads it
- the write side takes a struct pointer
- the read side returns a sentinel error
- attributes travel outbound only
- must run before the first response write
basics
~20 sBuild an http.Cookie value and pass it to http.SetCookie(w, c) before writing anything to the response; it adds a Set-Cookie header. On a later request, call r.Cookie("name"), which returns the cookie or the error http.ErrNoCookie.
solid answer
~40 sOn the way out you fill an `http.Cookie` struct — `Name`, `Value`, plus the attribute fields `Path`, `Domain`, `MaxAge`, `Secure`, `HttpOnly`, `SameSite` — and call `http.SetCookie(w, c)`. That function returns nothing; it just formats the struct and adds a `Set-Cookie` header, so it has to run before the first `w.Write` or `w.WriteHeader`, after which the header map is already flushed. On the way in, `r.Cookie("session")` returns `(*http.Cookie, error)` and the error is `http.ErrNoCookie` when the request carries no cookie by that name; `r.Cookies()` returns every cookie on the request. The cookie you read back has only `Name` and `Value` filled in, because a browser sends `Cookie: session=abc` with no attributes — `Path`, `MaxAge` and the flags are directives you send to the browser, not data it sends back.
code
go · 22 linesfunc login(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: newSessionID(),
Path: "/",
MaxAge: 3600,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
w.WriteHeader(http.StatusNoContent) // SetCookie had to come first
}
func whoami(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("session")
if err != nil { // http.ErrNoCookie when the header is absent
http.Error(w, "no session", http.StatusUnauthorized)
return
}
// c.Name and c.Value are set; c.MaxAge and c.Secure are zero values
fmt.Fprintln(w, c.Value)
}go deeper
Be ready to write the two calls from memory: http.SetCookie(w, &http.Cookie{...}) on the response, and r.Cookie("name") on the request with http.ErrNoCookie as the miss case.
Explain the ordering constraint — the header map is committed on the first write — and why an inbound cookie carries only Name and Value while attributes are outbound directives.
Show the judgment of setting Path and the flag fields explicitly on every cookie a service issues, rather than relying on zero values, so the emitted header is the same on every code path.
Be able to argue for one small helper that issues the service's cookies with agreed defaults, so scope and flags are set in one reviewed place instead of being retyped in each handler.
## The two halves of the API Go's `net/http` models a cookie as a plain struct, `http.Cookie`, and gives you one helper for each direction. **Writing.** `http.SetCookie(w http.ResponseWriter, cookie *http.Cookie)` formats the struct into a `Set-Cookie` header line and adds it to the response header. Note what it does *not* do: it returns no error, and it does not encode your value for you. Calling it twice adds two `Set-Cookie` lines, which is how you set several cookies in one response. Because it works by mutating the response header map, it must be called **before** the response header is written. The header is committed the first time you call `w.WriteHeader(...)` or the first time you call `w.Write(...)` (which implicitly writes a 200). A `http.SetCookie` after either of those is silently ineffective — the bytes are already on the wire. This is the single most common junior mistake with the API. **Reading.** `(*http.Request).Cookie(name string) (*http.Cookie, error)` looks through the request's `Cookie` header for a cookie with that exact name (names are case-sensitive) and returns it. When there is no such cookie the error is the sentinel `http.ErrNoCookie`, so the idiomatic handler is: ```go c, err := r.Cookie("session") if err != nil { // only possible error is http.ErrNoCookie } ``` `(*http.Request).Cookies()` returns `[]*http.Cookie` for everything the request carried. ## Why the cookie you read back looks empty A browser does not echo attributes. The response says: ``` Set-Cookie: session=abc123; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=Lax ``` but the next request says only: ``` Cookie: session=abc123 ``` So the `*http.Cookie` that `r.Cookie` hands you has `Name` and `Value` populated and everything else at its zero value. `Path`, `Domain`, `MaxAge`, `Expires`, `Secure`, `HttpOnly` and `SameSite` are *instructions to the browser* about when to send the cookie and who may read it; they are one-way. A handler that tries to read `c.Secure` or `c.MaxAge` off an inbound cookie to make a decision is reading a zero value, not a fact. The same struct type is used for both directions, which is what makes this confusing the first time. Think of `http.Cookie` as "the union of what you can send and what you can receive", with the receive side being a strict subset. ## The struct in practice ```go http.SetCookie(w, &http.Cookie{ Name: "session", Value: sid, Path: "/", MaxAge: 3600, HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode, }) ``` - `Path` scopes which URLs get the cookie back. Leave it empty and Go emits no `Path` attribute at all, which makes the browser scope the cookie to the directory of the URL that set it — rarely what you want. `Path: "/"` is the usual choice. - `Domain` left empty produces a host-only cookie for the exact host that set it. That is usually the right default; you only set it to share a cookie across subdomains. - `MaxAge` is a lifetime in seconds; leaving it at zero produces a cookie with no lifetime attribute, which lives until the browser session ends. - `Secure`, `HttpOnly` and `SameSite` are the flag fields. `SameSite` takes one of `http.SameSiteDefaultMode` (the zero value, which emits no attribute), `http.SameSiteLaxMode`, `http.SameSiteStrictMode` or `http.SameSiteNoneMode`. ## On the client side of the same struct The struct is symmetric for outbound *requests* too: `(*http.Request).AddCookie(c *http.Cookie)` appends a name/value pair to the request's `Cookie` header, and it deliberately ignores the attribute fields, since a client never sends them. That is the method to reach for when you are writing the caller rather than the server. ## What to remember `http.SetCookie` on the way out, before the first write, with no error to check. `r.Cookie` on the way in, with `http.ErrNoCookie` as the only error. Attributes travel outbound only.
- What exactly does r.Cookie return when the request carries no cookie of that name?A nil `*http.Cookie` and the sentinel error `http.ErrNoCookie`. That is the only error the method produces, so `if err != nil` is enough, though comparing with `errors.Is(err, http.ErrNoCookie)` documents the intent. Do not dereference the returned pointer before checking the error.
- Why are Path, MaxAge and Secure empty on the cookie you get from r.Cookie?Because the browser never sends them. A request's `Cookie` header is just `name=value` pairs; every attribute lives only in the outbound `Set-Cookie` header and is a directive to the browser about scope and lifetime. The inbound struct therefore has those fields at their zero values, and reading them tells you nothing.
- What happens if you call http.SetCookie after writing the response body?Nothing useful. The first `w.Write` or `w.WriteHeader` commits the status line and headers, so a later addition to the header map is never sent. The cookie silently fails to appear, with no error anywhere — set cookies at the top of the handler, before any output.
saying these in an interview costs you the question
- Calls http.SetCookie after writing the response body
- Reads c.Secure or c.MaxAge off an inbound request cookie
- Expects http.SetCookie to return an error to check
- Sets the Cookie header by hand instead of using http.SetCookie
- Thinks the browser echoes Path and SameSite back