skip to content

Writing Responses and Headers

Headers must be set before WriteHeader, the first Write silently sends 200, and a second WriteHeader is ignored with a log line. This is the handler bug an interviewer can describe in one sentence.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

In a Go http.Handler, why must w.Header().Set calls come before WriteHeader or Write?

level: juniorimportance: must knowfreq 72%

answer

  1. headers ride along with the status line
  2. the first Write is not free
  3. Write implies WriteHeader(200)
  4. set headers, then status, then body

basics

~20 s

The header map is only copied onto the wire when the status line is sent. w.WriteHeader sends it, and the first w.Write sends it implicitly with status 200, so any Header().Set after that point is silently ignored.

solid answer

~40 s

`w.Header()` hands you a mutable map that `net/http` buffers, and that map becomes the real response headers at the instant the status line goes out. That instant is `w.WriteHeader(code)`, or the first `w.Write`, which calls `WriteHeader(http.StatusOK)` for you. Once the headers are on the connection a later `Set` or `Del` mutates a map nobody will read again: no error, no panic, just a header the client never receives. So every handler follows one order — set all headers, then the status, then the body. The documented exceptions are 1xx informational responses and trailers, which are declared before the body and written after it. Anything that needs to add a header around code that writes the body has to act first, or wrap the `http.ResponseWriter`.

code

go · 9 lines
go
func flags(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Cache-Control", "no-store")
	w.WriteHeader(http.StatusOK) // headers are serialised here
	w.Write([]byte(`{"dark_mode":true}`))

	// Too late: the map is no longer consulted, and no error is reported.
	w.Header().Set("X-Flag-Version", "42")
}

go deeper

for a junior

Be ready to say the order out loud: headers, then status code, then body. Know that a w.Write with no prior WriteHeader sends 200 and locks the headers at that moment.

for a middle

Explain that w.Header() is an in-memory map serialised exactly when the status line goes out, name what the server fills in for you, and name the exceptions: trailers and 1xx responses.

for a senior

Show how you keep this right in real code: one write path per handler, all headers decided before serialisation begins, and a ResponseWriter wrapper when a layer must add headers around a handler it does not own.

for a principal

Own the convention rather than the instance. A house response helper that encodes header-then-status-then-body, plus a review habit of reading handlers backwards from the last write, removes a class of bug whose only symptom is a client complaint.

### The three things a handler writes, in order `http.ResponseWriter` has exactly three methods, and they correspond to the three parts of an HTTP response, in the order the response goes out: - `Header() http.Header` — a map you may edit freely, but only *until* the status line is written. - `WriteHeader(statusCode int)` — sends the status line **and the whole header map** as it stands at that moment. - `Write([]byte) (int, error)` — sends body bytes; if `WriteHeader` has not been called yet, it calls `WriteHeader(http.StatusOK)` first. That third bullet is where nearly all real bugs come from. Writing a body is not a neutral act: the first byte you write commits the status code and freezes the headers. ### What "frozen" actually means `w.Header()` is not a view onto something already sent — it is a plain `map[string][]string` the server keeps in memory. When the status line is written, `net/http` serialises that map into the response and stops consulting it. Mutating it afterwards is a perfectly legal map write, so nothing complains. The failure is invisible on the server and shows up only as a missing header in a client's logs, which is why this bug is usually reported from the outside rather than caught in review. ### The automatic headers the server adds for you At the same moment, the server fills in what you left out: - **Content-Type**, if you set none, by passing up to the first 512 bytes you wrote to `http.DetectContentType`. - **Content-Length**, if the entire response was small enough to be buffered before the handler returned and nothing forced an early flush; otherwise the response is chunked. - **Date**, and the connection-management headers. Because Content-Type is derived from the first bytes of the body, setting it after that first `Write` is doubly useless: the header map is closed *and* the server has already guessed. ### The order that always works ```go w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusOK) w.Write(body) ``` Headers, then status, then body. If you cannot decide the status until you have produced the body — for example when serialising might fail — produce the body into memory first (`json.Marshal`, a `bytes.Buffer`), then run the three steps above. Deciding the status after you have already streamed bytes is not something the protocol lets you take back. ### The exceptions worth knowing **Trailers.** HTTP/1.1 chunked responses and HTTP/2 allow headers *after* the body. In Go you either declare them up front (`w.Header().Set("Trailer", "X-Checksum")`, then set `X-Checksum` after writing the body) or use the `http.TrailerPrefix` convention, setting `w.Header().Set(http.TrailerPrefix+"X-Checksum", v)` after the body. Trailers are the only headers a client will see if you set them late, and many clients ignore them. **1xx responses.** Informational responses are sent ahead of the final one and do not close the header map for the real response. **Suppressing an automatic header.** Assigning `nil` to a key in `w.Header()` tells the server not to generate that header at all — the documented way to stop it from adding one it would otherwise supply. ### Consequences for wrappers Anything that wraps a handler — a wrapper that records the status, a wrapper that adds a correlation header — has to do its header work *before* the inner handler writes. Once the inner handler has written a byte, an outer layer holding the same `http.ResponseWriter` has no way to add anything: the header map it can still reach is no longer the response. In practice that means either setting the header on the way in, or supplying a `ResponseWriter` implementation that injects the header inside its own `WriteHeader` before delegating to the real one. ### How to spot it in review Read a handler backwards from its last `Write`. Every `Header().Set` below the first write or the first `WriteHeader` — including one inside an error branch, and including `http.Error`, which sets three headers of its own — is dead code. That reading takes seconds and catches the whole class.

  • If the handler never calls WriteHeader, what sends the status?
    The first `w.Write` does: `net/http` calls `WriteHeader(http.StatusOK)` before writing your bytes, and at the same moment fills in Content-Type by sniffing the body and, for a small buffered response, Content-Length. A handler that returns without writing anything still produces a 200 with an empty body.
  • Are there headers you can legitimately set after WriteHeader?
    Trailers and headers on 1xx informational responses. Trailers must either be announced in the Trailer header before the body or set using the `http.TrailerPrefix` key convention, and they only travel on chunked HTTP/1.1 or HTTP/2 responses. Everything else set after the status line is discarded.
  • Why does a late Header().Set never return an error?
    `Header()` returns the live `http.Header` map and `Set` is an ordinary map write with no return value; the server simply stops reading that map once the headers are serialised. The only feedback is behavioural — a header the client does not receive — which is why these bugs usually surface as an external report.

saying these in an interview costs you the question

  • Thinks Header().Set works any time before the handler returns
  • Assumes a late Set returns an error or panics
  • Believes Write sends only the body, not the headers
  • Calls WriteHeader first, then sets Content-Type
  • Expects net/http to re-read the header map when the handler returns
open as a page

What do http.Error and http.Redirect write to a response besides the status code?

level: middleimportance: should knowfreq 52%

basics

~10 s

http.Error clears Content-Length, forces Content-Type to text/plain; charset=utf-8, adds X-Content-Type-Options: nosniff, sends the status, then writes the message and a newline. http.Redirect sets Location and, for a GET, writes a small HTML link page.

open as a page

Errors from a Go JSON endpoint arrive as 200 and the server logs a superfluous WriteHeader call. Why?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The handler encoded straight to the ResponseWriter, so its first byte sent an implicit 200. The later http.Error could not change a status already on the wire, so net/http logged the superfluous call and appended the message to the partial body.

open as a page

How does Go's net/http choose a Content-Type when the handler sets none?

level: middleimportance: nice to knowfreq 35%

basics

~20 s

On the first write, net/http passes up to the first 512 bytes of the body to http.DetectContentType and sends whatever that returns. The sniffer has no JSON signature, so a JSON body is usually labelled text/plain; charset=utf-8.

open as a page