What does httptest.ResponseRecorder.Result() return, and when is it safe to call?
answer
- it hands you what a client would have seen
- one of the fields is frozen early
- the headers freeze at the first write
- the status starts life as 200
- read it only once the handler has returned
basics
~20 sResult builds an *http.Response from what the handler recorded: status, a header snapshot taken at the first write, and a readable body. Call it only after the handler has finished running, then assert on that response.
solid answer
~50 s`Result()` turns the recorder's raw state into the `*http.Response` a client would have seen, which is the surface you should assert against — `StatusCode`, `Header`, `Body`, `ContentLength`. Three details decide whether your assertions are honest. First, it must be called **after** the handler has returned; calling it mid-flight races with the handler and the docs forbid it. Second, `Header` is a **snapshot taken at the first write** (or at the call, if the handler never wrote), so a header set after `w.Write` will not appear — exactly like a real server, which has already flushed the headers. Third, `httptest.NewRecorder` seeds `Code` to 200, so a handler that returns without writing anything still reports 200; a test that only checks for 200 can pass against a handler that did nothing. The body is a fresh reader over the recorded bytes, so read it, and prefer it over the deprecated `HeaderMap` field.
code
go · 18 linesrec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("POST", "/v1/charges", strings.NewReader(`{}`)))
res := rec.Result()
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
t.Fatalf("status = %d, want 201", res.StatusCode)
}
if ct := res.Header.Get("Content-Type"); ct != "application/json" {
t.Errorf("Content-Type = %q", ct)
}
// A header the handler sets after its first write is absent here,
// exactly as it would be absent on the wire.
var out struct{ ID, Status string }
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
t.Fatalf("decode: %v", err)
}go deeper
Remember the call order: run the handler first, then read Result(), then assert on StatusCode and the body. Do not inspect the recorder while the handler is still executing.
Explain the mechanics: the header snapshot is taken at the handler's first write, Code is seeded to 200 by NewRecorder, and Body comes back as a fresh readable stream over the recorded bytes.
Show that you use these details to catch real bugs — a header set after the body is a production defect the recorder faithfully reproduces, and a lone 200 assertion is a test that cannot fail.
Argue for assertion conventions a team can hold to: assert on decoded fields and named headers rather than whole-response equality, so tests survive library upgrades without pinning internals.
## What the recorder actually holds `httptest.NewRecorder()` returns a `*httptest.ResponseRecorder` with a small set of exported fields: - `Code int` — the status the handler set, **seeded to 200** by `NewRecorder`. - `Body *bytes.Buffer` — everything written through `Write`/`WriteString`. - `Flushed bool` — whether the handler called `Flush`. - `HeaderMap http.Header` — the live header map. It is **deprecated**: it is the map the handler mutates, not the headers that were sent. And one method that matters more than all of them: `Result() *http.Response`. ## What `Result()` builds `Result` assembles an `*http.Response` describing what a client would have received: - `StatusCode` from `Code`, plus a matching `Status` string. - `Header` — a **snapshot** of the header map, taken at the moment of the handler's first write (`WriteHeader` or `Write`), or at the time `Result` is called if the handler never wrote at all. - `Body` — a non-nil `io.ReadCloser` over the recorded bytes. It never fails with anything but `io.EOF`, but you should still read it and close it, because that is how you would treat any `*http.Response` and it keeps the test honest when the code is refactored to hit a real server. - `ContentLength`, derived from the `Content-Length` header if the handler set one, plus trailers where present. The documentation adds a rule worth quoting to a reviewer: more fields may be populated in future, so **do not `reflect.DeepEqual` a whole `*http.Response`**. Assert on the specific fields you care about. ## The three traps ### 1. Call it after the handler has finished `Result` must only be called once the handler has returned. In a straight-line test that is automatic — you call `ServeHTTP` and then `Result`. It stops being automatic when the handler spawns a goroutine that keeps writing, or when a test helper captures the recorder and inspects it from another goroutine. In both cases you are reading state that the handler is still mutating, and with `-race` you will see it as a data race. A handler that writes to the `ResponseWriter` after `ServeHTTP` returns is a bug in its own right — the real server's writer is invalid at that point. ### 2. The header snapshot is taken at the first write ```go func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "{}") // headers snapshot here w.Header().Set("X-Trace-Id", "abc") // too late } ``` `rec.Result().Header.Get("X-Trace-Id")` is empty. This is not a quirk of the recorder — it is faithful emulation. A real `net/http` server writes the status line and headers on the first write, so anything you set afterwards never reaches the client. The recorder makes the bug visible instead of hiding it, which is exactly why you assert on `Result().Header` and not on the live `HeaderMap`, where the late header *would* show up and your test would wrongly pass. ### 3. `Code` starts at 200 `NewRecorder` seeds `Code` to 200 so that a handler which writes a body without calling `WriteHeader` records the same status a real server would send. The side effect is that a handler which returns *without writing anything* also records 200. If your assertion is `if rec.Code != 200 { t.Fatal(...) }`, an empty handler passes. Guard against it by also asserting on the body, the `Content-Type`, or a specific field of the decoded payload. ## Asserting well on a recorded response For a wrapper around a third-party API that hands back JSON of unknown shape, decode into the type you actually promise your callers and compare fields, rather than string-matching raw JSON — key order and whitespace are not part of the contract. When a test fails and you need to see everything at once, `httputil.DumpResponse(res, true)` prints the status line, headers and body in wire form, which is usually faster than adding four more `t.Logf` calls. A final note on `Flushed`: it tells you the handler called `Flush`, which is how you test a streaming or server-sent-events handler's chunking intent. The recorder does not chunk anything; it only records that the flush happened. ## Summary `Result()` is the recorder's supported read surface. Call it after the handler returns, remember that its headers were snapshotted at the first write, and never let a bare 200 assertion stand alone.
- A handler sets a header after calling w.Write and the test asserting on it fails. Is the test wrong?No — the handler is. A server sends the status line and headers on the first write, so a header set afterwards never reaches the client, and `Result().Header` reflects that by snapshotting at the first write. The fix is to set every header before writing the body. Asserting on the deprecated `HeaderMap` would hide the bug, because that live map does show the late header.
- Why should you not compare a whole *http.Response from Result() with reflect.DeepEqual?The documentation says more fields may be populated in future releases, so a deep comparison pins your test to today's internals and breaks on an upgrade that changed nothing you care about. Assert on the specific fields — status, chosen headers, decoded body — that form your actual contract.
- What does the recorder's Flushed field tell you?That the handler called Flush on the ResponseWriter. It is how you assert that a streaming handler pushed data out at the points it intended. The recorder does not chunk or send anything; it only records that the flush occurred, so it proves intent, not wire behaviour.
saying these in an interview costs you the question
- Reading Result() while the handler is still running
- Asserting on the deprecated HeaderMap field instead of Result().Header
- Expecting a header set after the first write to appear
- Checking only for status 200, which a no-op handler also records
- Comparing the whole *http.Response with reflect.DeepEqual