skip to content

What does Go's http.SetCookie do to a cookie value containing a space, a semicolon or a quote?

level: middleimportance: nice to knowfreq 38%

answer

  1. no percent encoding happens anywhere
  2. spaces and commas trigger double quotes
  3. illegal bytes are dropped, not escaped
  4. an invalid name emits nothing at all
  5. encode the value yourself first

basics

~20 s

Go sanitises rather than escapes: a value containing a space or a comma is wrapped in double quotes, and never-legal bytes such as semicolons, backslashes and quotes are dropped with a log line. Nothing is percent-encoded.

solid answer

~50 s

`net/http` runs the value through a sanitiser before writing it. Illegal bytes — control characters, `;`, `\`, `"` and anything outside printable ASCII — are **dropped**, with a line logged to the standard logger; they are not escaped and you get no error. What is left is then wrapped in double quotes if it contains a space or a comma, which is why `Value: "dark mode"` comes out as `pref="dark mode"`. Parsing reverses that: Go strips the quotes and records the fact in the `Quoted` field. The `Name` is stricter — if it is not a valid token, `(*http.Cookie).String()` returns the empty string and `http.SetCookie` writes no header at all, silently, since it returns nothing. So encode anything that is not a plain token yourself, and call `(*http.Cookie).Valid()` if you want to be told about a bad struct.

code

go · 3 lines
go
c := &http.Cookie{Name: "pref", Value: "dark mode; drop=me"}
fmt.Println(c.String())
// pref="dark mode drop=me"   (the ';' was dropped and logged)

go deeper

for a junior

Know that Go does not encode cookie values for you: encode anything that is not plain letters, digits and dashes yourself, typically with base64 URL encoding.

for a middle

Explain the two distinct behaviours — illegal bytes dropped with a log line, and quoting triggered only by a space or a comma — and that an invalid name emits no header at all.

for a senior

Demonstrate defending against the silent paths: validate with Cookie.Valid, assert the emitted line in a test, and keep values opaque so the sanitiser never has anything to remove.

for a principal

Be ready to set the convention that everything a service puts in a cookie is an encoded opaque token, and to say what that buys in debuggability versus a readable value.

## Sanitising is not escaping A cookie value lives in a header whose grammar reserves several characters — `;` separates attributes, `,` separates cookies in some contexts, `"` delimits a quoted value, and control characters could split the header entirely. A library has three options: reject, escape, or strip. Go strips. When `http.SetCookie` formats the struct, the value goes through a sanitiser that walks the bytes and keeps only the legal ones. Roughly, a byte is legal if it is printable ASCII and is not `"`, `;` or `\`. Everything else is **removed from the value**, and `net/http` logs a line through the standard logger saying it found an invalid byte and is dropping invalid bytes. It does not percent-encode, it does not backslash-escape, and it does not return an error — `http.SetCookie` has no error to return. So this: ```go c := &http.Cookie{Name: "pref", Value: "dark mode; drop=me"} ``` loses the semicolon and becomes `dark mode drop=me`, which then gets quoted (see below) and is emitted as `pref="dark mode drop=me"`. The value that comes back on the next request is not the value you set — a real source of "the token in the database does not match the token in the cookie" bugs when someone puts structured text straight into a cookie. ## Quoting After stripping, if what remains contains a **space or a comma**, Go wraps the whole thing in double quotes. That is the only condition; nothing else triggers quoting. `Value: "a b"` produces `name="a b"`, while `Value: "a-b"` produces `name=a-b`. The reverse direction handles this too: when Go parses an inbound cookie whose value is wrapped in double quotes, it removes them and sets the `Quoted` field on the `http.Cookie` to true, so the parsed `Value` is the clean text. Re-emitting that struct puts the quotes back, which keeps a proxy or a test round trip faithful. You can set `Quoted` yourself to force quoting on a value that would not otherwise need it. ## The name is stricter, and fails silently A cookie **name** must be a valid HTTP token: no spaces, no `=`, no separators, no control bytes. Go does not sanitise a bad name into a good one. `(*http.Cookie).String()` returns the empty string, and `http.SetCookie` only adds a header when `String()` is non-empty — so an invalid name means **no `Set-Cookie` header at all**. There is no error, no panic, and the handler proceeds as if everything worked. `Name: "user id"` is a header that never appears. (The name does get one small fix-up: carriage returns and newlines are replaced with `-` rather than dropped, specifically so a name can never split the header.) ## How to find out that it happened Three tools, in increasing order of effort: 1. `(*http.Cookie).Valid() error` checks the struct and tells you *which* field is wrong — name, value bytes, path, domain or expires. Nothing calls it for you; call it yourself when the value comes from anywhere but a constant. 2. `(*http.Cookie).String()` gives you the exact header line `SetCookie` would emit. Printing or asserting it in a test is the fastest way to see a dropped byte. 3. The dropped-byte log line goes to the standard logger, so it appears in the server's output — worth grepping for when a value mysteriously changes shape. ## The rule that avoids the whole area Do not put arbitrary text in a cookie value. Encode it: - `base64.RawURLEncoding.EncodeToString(b)` for opaque bytes — the alphabet is entirely legal in a cookie and needs no quoting. - `url.QueryEscape(s)` for text you want to read back with `url.QueryUnescape`. - Anything structured — JSON, a struct, several fields — is better encoded once and decoded once than shoved in raw, because the sanitiser will happily eat the punctuation that gives it structure. With an encoded value, quoting never triggers, nothing is ever dropped, and what you read back is byte-for-byte what you wrote.

  • How should you carry an opaque session token that is raw bytes in a Go cookie value?
    Encode it before assignment — `base64.RawURLEncoding.EncodeToString(b)` is the usual choice, since its alphabet is entirely legal in a cookie value, needs no quoting and survives the sanitiser untouched. Decode with the matching `DecodeString` after `r.Cookie`. Raw bytes assigned directly will lose whatever the sanitiser considers illegal.
  • How do you find out that http.SetCookie refused to emit your cookie?
    Call `(*http.Cookie).Valid()` yourself; it reports which field is invalid. `SetCookie` returns nothing and only adds a header when `(*http.Cookie).String()` is non-empty, so an invalid name disappears silently. In a test, assert on `String()` or on `rec.Result().Cookies()` from an `httptest` recorder.
  • What is the Quoted field on http.Cookie for?
    It records that the parsed value arrived wrapped in double quotes, so `Value` holds the unquoted text while a re-emit reproduces the original header faithfully. You can also set it to force quoting on a value that would not otherwise need it. Without it, a parse-then-emit round trip would silently change the header.

saying these in an interview costs you the question

  • Believes Go percent-encodes cookie values automatically
  • Expects an error when the value has illegal bytes
  • Thinks a bad cookie name panics or is fixed up
  • Stores raw JSON in a cookie value unencoded
  • Assumes quoting happens for every value with punctuation