Why must multiple HTTP 'Set-Cookie' response headers never be folded into a single comma-separated header line, and what does that imply for HTTP client library APIs?
answer
- Expires HTTP-date contains a comma → folding is ambiguous
- Set-Cookie is not a comma-separated list header
- need getAll / array / getSetCookie(), not getHeader
- addHeader not setHeader in servlet loops
- HTTP/2-3: never join set-cookie; DO rejoin split cookie requests with '; '
basics
~20 sThe Expires attribute uses an HTTP-date that itself contains a comma ('Wed, 09 Jun 2027 10:18:14 GMT'), so comma-joining Set-Cookie values is ambiguous and cannot be split back reliably. Client libraries must expose a get-all-values API; a single getHeader('Set-Cookie') loses cookies or corrupts them.
solid answer
~50 sHTTP normally allows repeated header fields to be combined into one field with comma-separated values. `Set-Cookie` is the famous exception: its `Expires` attribute is an `HTTP-date` such as `Wed, 09 Jun 2027 10:18:14 GMT`, which **contains a comma**. Fold two `Set-Cookie` values together and no parser can reliably tell a cookie boundary from a comma inside a date. RFC 6265 therefore says origin servers should not fold multiple `Set-Cookie` fields, and receivers must handle them as separate fields.\n\nThe API consequence is concrete: any header accessor that returns a single string per name is wrong for `Set-Cookie`. Libraries need `getAll`/`getHeaders`, a multimap, or a dedicated cookie jar — Node's `res.headers['set-cookie']` returns an array precisely for this reason, and `fetch`'s `Headers` needed `getSetCookie()` added because its normal `get()` joins values with commas.\n\nThe same holds in HTTP/2 and HTTP/3: multiple `Set-Cookie` field lines are carried separately, and any intermediary that concatenates them is broken.
code
http · 1 lineHTTP/1.1 200 OK\nSet-Cookie: sid=abc; Expires=Wed, 09 Jun 2027 10:18:14 GMT; Path=/\nSet-Cookie: csrf=xyz; Expires=Wed, 09 Jun 2027 10:18:14 GMT; Path=/go deeper
Know that each cookie needs its own Set-Cookie header and that they must not be merged.
Give the reason — the comma inside the Expires date — and name the API consequence, such as Node returning an array or fetch needing getSetCookie().
Describe the failure mode end to end (missing CSRF cookie after login), where it hides (gateways, mocks), and how you would test for it.
Treat it as an interoperability constraint on your platform's HTTP abstractions: no single-string header maps on the response path, and conformance tests at every proxy hop.
## The general rule and the exception\n\nHTTP defines most repeated header fields as list-valued: `Accept: a` plus `Accept: b` is semantically identical to `Accept: a, b`, and proxies, servers and libraries are free to combine them. That single simplification is what lets header APIs be a plain `Map<String, String>`.\n\n`Set-Cookie` breaks it. Its grammar is not a comma-separated list, and its `Expires` attribute is an `HTTP-date`:\n\n```\nSet-Cookie: a=1; Expires=Wed, 09 Jun 2027 10:18:14 GMT\nSet-Cookie: b=2; Expires=Thu, 10 Jun 2027 10:18:14 GMT\n```\n\nFolded, that becomes:\n\n```\nSet-Cookie: a=1; Expires=Wed, 09 Jun 2027 10:18:14 GMT, b=2; Expires=Thu, 10 Jun 2027 10:18:14 GMT\n```\n\nSplitting on commas now yields four fragments, two of them meaningless. A parser can only recover the cookies with a heuristic — "a comma followed by something that looks like a weekday abbreviation and a date is not a boundary" — and heuristics fail on values that legitimately resemble dates. RFC 6265 accordingly instructs origin servers not to fold, and receivers to treat each `Set-Cookie` as its own field.\n\nNote the direction asymmetry: the **request** `Cookie` header *is* a single header with pairs separated by `\";\"`, so the problem is specific to the response side.\n\n## What this breaks in practice\n\n**Single-string header APIs.** Any API shaped `String getHeader(String name)` silently loses cookies when a response sets more than one. Real failures follow the same script: login sets a session cookie and a CSRF cookie; the client library returns only the first (or a mangled join); the CSRF cookie is missing; every subsequent POST is rejected; someone spends a day blaming CSRF configuration.\n\nCorrect APIs:\n\n- Node.js: `res.headers['set-cookie']` is an **array** — the one header special-cased in the HTTP parser.\n- Fetch/`Headers`: `get('set-cookie')` joins with commas, which is why **`getSetCookie()`** was added to the spec and shipped in browsers and runtimes.\n- Java servlet: `HttpServletResponse.addHeader(\"Set-Cookie\", …)` (append) versus `setHeader` (replace) — using `setHeader` in a loop is a classic way to end up with exactly one cookie.\n- Most languages also expose a cookie jar that does the parsing for you; prefer it over hand-splitting.\n\n**Proxies and gateways.** An intermediary that normalises headers into a map, or that helpfully joins repeated fields, will corrupt cookies. This shows up as "cookies work locally but not behind the gateway" and is worth testing explicitly with a response that sets two cookies, one of them carrying `Expires`.\n\n**Test doubles.** Mock HTTP layers that model headers as a dictionary hide the bug until production.\n\n## HTTP/2 and HTTP/3\n\nBinary framing does not change any of this. In HTTP/2 and HTTP/3 each `set-cookie` field line is carried as its own header field in the HPACK/QPACK block; a receiver must not concatenate them into one value. The specifications go out of their way to say so, because the naive "join repeated fields with a comma" rule that works for other headers would corrupt cookies here.\n\nThe **request** side gets its own special rule in the opposite direction: HTTP/2 and HTTP/3 permit the `cookie` header to be **split** across multiple field lines to improve compression efficiency, and require the receiver to rejoin them with `\"; \"` before handing the value to a generic HTTP/1.1-shaped API. So for cookies, the response header must never be joined and the request header must always be joined — the exact opposite of each other, which is why cookie handling belongs in a library rather than in your own code.\n\n## What to say and what to do\n\nState the reason (comma inside the `Expires` HTTP-date), state the rule (never fold `Set-Cookie`; each is its own field), and state the practical test: does your client expose a multi-value accessor? If your stack forces you to parse a joined string, use a maintained cookie parser rather than a regular expression — the date heuristic is exactly the kind of code that works for a year and then eats a session cookie.
- HTTP/2 allows the request cookie header to be split across several field lines. Does that contradict the no-folding rule?No — the rules apply to different headers in different directions. The response set-cookie field lines must stay separate because their values are not a comma list. The request cookie header is a single logical value that HTTP/2 may split for better header compression, and the receiver must rejoin the pieces with '; ' before handing it to an HTTP/1.1-style API.
- Your client library only exposes getHeader(name) returning one string. What now?Reach for whatever multi-value accessor exists underneath — an array, a header multimap, or a cookie jar — before parsing anything yourself. If you truly must split a joined string, use a maintained cookie parser that special-cases the Expires date rather than splitting on commas, and add a test with a response setting two cookies that both carry Expires.
saying these in an interview costs you the question
- Claiming Set-Cookie can be folded like any other list header
- Blaming the semicolon rather than the comma inside the Expires HTTP-date
- Splitting a joined Set-Cookie string on commas with a regex and calling it done
- Using setHeader instead of addHeader when emitting several cookies
- Assuming HTTP/2 changes the rule because headers are binary