How does http.Client.Jar change the cookies Go sends across a chain of redirects to different hosts?
answer
- cookies are not just a header you set
- each hop is asked about its own URL
- 3xx responses carry Set-Cookie too
- sensitive headers stop at a host change
- nil Options means no public suffix list
basics
~20 shttp.Client.Jar makes the client store every Set-Cookie it receives, including on redirect responses, and send the cookies matching each hop's own URL. With a nil Jar nothing is remembered, and a hand-set Cookie header is dropped once the hop leaves the original domain.
solid answer
~50 sWith `Client.Jar` nil — the default — the client keeps no cookie state at all. It forwards the headers you set on the original request to later hops, but treats `Authorization`, `WWW-Authenticate` and `Cookie` as sensitive: they are dropped when the redirect target is neither the same host nor a subdomain of the original. So a login flow that depends on a `Set-Cookie` from the 302 simply loses the session. Set `Jar` to a `*cookiejar.Jar` and the behaviour changes on both sides: every response in the chain, including the 3xx hops, has its `Set-Cookie` headers stored, and every outgoing hop is given the jar's cookies for **that hop's** URL. The jar scopes cookies by domain using its `PublicSuffixList`; passing nil `Options` to `cookiejar.New` is allowed but insecure, because with no list one host can set a cookie for another.
code
go · 10 lines// Nil Options is allowed but insecure: with no public suffix list, one
// host can set a cookie the jar will offer to another. Supply a real list.
jar, err := cookiejar.New(nil)
if err != nil {
return err
}
client := &http.Client{Jar: jar}
// Every hop is sent the jar's cookies for that hop's URL, and every
// response in the chain has its Set-Cookie headers stored back.go deeper
Remember that a Go client keeps no cookies unless you give it a jar, and that the jar goes in the Jar field of http.Client. Know that net/http/cookiejar provides the standard implementation.
Explain the two-method CookieJar interface and when the client calls each one, including on the redirect responses you never see. Say which headers count as sensitive and what the subdomain-match rule means.
Diagnose a session or token that vanishes mid-chain: log each hop, identify the host change, and choose between refusing the redirect and allowlisting the target. Treat a shared jar as shared mutable state with a lifetime.
Set the posture for outbound clients that carry credentials: whether host changes are permitted at all, who owns the allowlist, and whether jars are scoped per identity so one dependency's cookies can never reach another's host.
## Two independent mechanisms Cookies reach an outbound Go request by two routes, and a redirect chain is where the difference becomes visible. **Route one: a header you set.** `req.Header.Set("Cookie", "session=abc")` puts a literal header on the first request. The client forwards headers from the initial request onto redirect hops, with one exception: sensitive headers — `Authorization`, `WWW-Authenticate`, `Cookie` — are withheld when the target is not an exact or subdomain match of the initial domain. A redirect from `foo.com` to `foo.com` or `sub.foo.com` keeps them; a redirect to `bar.com` does not. Nothing else is remembered, and no `Set-Cookie` from any hop is honoured. **Route two: a jar.** `http.Client.Jar` is an interface: ``` type CookieJar interface { SetCookies(u *url.URL, cookies []*Cookie) Cookies(u *url.URL) []*Cookie } ``` When it is non-nil the client calls `Cookies(u)` before sending each request, including each redirect hop, and adds what comes back; and it calls `SetCookies(u, ...)` with the `Set-Cookie` headers of each response it receives, including the 3xx responses it consumes on your behalf. `net/http/cookiejar` provides the standard in-memory implementation. The consequence is that a jar makes a redirect chain behave the way a browser does. Hop one returns 302 with `Set-Cookie: session=...`; the jar stores it; hop two is sent to the target with that cookie attached because the jar's rules say it applies to that URL. ## Per-hop, not per-request The crucial detail is that `Cookies(u)` is asked about **each hop's own URL**, not about the URL you originally called. Cross-host redirects therefore do the right thing automatically: cookies scoped to `auth.example.com` go to `auth.example.com` and are simply not offered to `cdn.other.com`. You do not filter anything yourself. When both routes are in play — a jar *and* a hand-written `Cookie` header — the client forwards the header but omits any cookie the jar has since changed, on the assumption that the jar will supply the current value for hosts it covers. Mixing the two is a good way to confuse yourself; pick one. ## The public suffix list matters `cookiejar.New` takes `*cookiejar.Options`, whose only field is `PublicSuffixList`. That list decides which domain suffixes are registrable, and therefore whether a server is allowed to set a cookie for a broader domain. Passing nil is legal and useful in tests, but the package documents it as insecure: without a list, a server for one domain can set a cookie that the jar will then hand to an unrelated domain. A crawler that follows redirects to arbitrary hosts with a nil-options jar is a cookie-mixing hazard, so supply a real list. ## Why the sensitive-header rule bites The rule is about credentials leaving the audience they were minted for. A client that sets `Authorization: Bearer ...` and follows a redirect to a host it does not control would otherwise hand that token to a stranger; a redirect target is chosen by the *server*, not by you. Go therefore drops it, silently, and the symptom is a confusing 401 from the second hop. The diagnosis is to log each hop from a `CheckRedirect` callback and see that the header is no longer there. If a downstream genuinely needs credentials after a host change, do not fight the rule by re-adding the header in `CheckRedirect` unless you have decided the new host is trustworthy — that decision is the whole point of the rule, and it should be an allowlist, not a blanket re-add. ## What to do in practice - Any client that must survive a login redirect gets a `*cookiejar.Jar` with a real public suffix list. Share it deliberately: a jar is shared mutable state, so one jar per identity, not one global jar for every caller. - A client that carries a bearer token should usually also carry a `CheckRedirect` that refuses to change host, so that a moved endpoint fails loudly rather than silently unauthenticated. - A crawler that walks untrusted links should either use no jar at all, or a per-target jar, so one site's cookies cannot follow a redirect into another crawl. - Remember that `resp.Cookies()` only parses the final response's headers. It is not the chain's cookies; the jar is. ## The porting trap Someone arriving from a command-line HTTP client expects redirect following and cookie retention to come as a pair, because that is how the tools they used behaved. In Go, redirect following is on by default and cookie retention is off by default. That asymmetry is the root of most sessions that mysteriously vanish somewhere in the middle of a chain.
- A bearer token is set on the request and the second hop returns 401. What happened?The redirect changed host to something that is neither the original host nor a subdomain of it, so the client withheld the Authorization header as a sensitive header. Confirm by logging each hop from a CheckRedirect callback. The fix is a decision, not a workaround: either refuse the host change, or re-add the header only for hosts on an explicit allowlist.
- Why does a login flow that works from a shell tool lose its session in Go?Because Go follows redirects by default but keeps no cookies by default. The 302 that carried Set-Cookie is consumed by the client and thrown away, so the next hop arrives unauthenticated. Set http.Client.Jar to a *cookiejar.Jar and the same flow works, because the jar stores cookies from every response in the chain.
- Should a crawler share one jar across all the sites it visits?No. A jar is shared mutable state and its whole job is to carry cookies forward, so one global jar lets a redirect move state between unrelated targets and grows without bound. Use a jar per target or per identity, always with a real PublicSuffixList, and drop it when that crawl finishes.
- Does resp.Cookies() give you the cookies gathered across the chain?No. Response.Cookies parses the Set-Cookie headers of that one response, which after redirects is the final hop only. Cookies set by the intermediate 3xx responses are not there; they went into the jar, if you configured one, and are otherwise gone.
A hand-set Cookie header is a note you staple to the first envelope; a jar is an address book the client consults freshly at every stop on the route.
saying these in an interview costs you the question
- Assumes Go keeps cookies across redirects by default
- Thinks a hand-set Cookie header survives any redirect
- Uses one global jar for every outbound caller
- Passes nil Options to cookiejar.New in production
- Expects resp.Cookies to hold the whole chain's cookies