Why must access-log middleware wrap http.ResponseWriter to record a response's status code?
answer
- the interface only sends, never reports
- you can only see what you pass in
- embed the writer, override two methods
- Write returns the byte count you need
- hand the handler your struct instead
basics
~20 shttp.ResponseWriter is write-only: Header, Write and WriteHeader, with no getter for what was sent. Middleware passes the handler a struct that embeds the writer and overrides those methods, so it can record the status code and the byte count.
solid answer
~40 s`http.ResponseWriter` has exactly three methods — `Header()`, `Write([]byte) (int, error)` and `WriteHeader(int)` — and none of them reads anything back, so after `next.ServeHTTP(w, r)` returns there is nowhere to ask what status went out. The standard move is to define a small struct that embeds `http.ResponseWriter`, override `WriteHeader` to save the code before forwarding, and override `Write` to add the returned `n` to a byte counter, then pass a pointer to that struct to the inner handler instead of the original writer. Because the struct embeds the interface, `Header()` and anything you did not override are promoted straight through, so the handler behaves normally. After the handler returns, the middleware reads the recorded fields together with the elapsed time and emits one log line or one metric per request.
code
go · 16 linestype recorder struct {
http.ResponseWriter
status int
nbytes int64
}
func (r *recorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
func (r *recorder) Write(b []byte) (int, error) {
n, err := r.ResponseWriter.Write(b)
r.nbytes += int64(n)
return n, err
}go deeper
Be ready to say that http.ResponseWriter only has Header, Write and WriteHeader, and to sketch a struct that embeds it and overrides WriteHeader to save the code before forwarding it.
Explain why embedding forwards the untouched methods, why the wrapper is passed by pointer, and exactly which bytes the counter from Write does and does not include.
Show the judgment that this belongs in one shared layer rather than in each handler, and name what the wrapper changes for the handler underneath it — the value it holds is now your type, not net/http's.
Own the argument that per-request observability is a platform concern: one wrapper, one metric contract, and a clear statement of what its byte and status numbers mean so dashboards across teams compare like with like.
## The interface gives you no way to look back `http.ResponseWriter` is deliberately tiny: - `Header() http.Header` — the response header map, mutable until the first write. - `Write([]byte) (int, error)` — writes body bytes, and sends the status line and headers first if they have not gone out yet. - `WriteHeader(statusCode int)` — sends the status line and headers. Every one of those is an instruction to *send* something. There is no `Status()`, no `BytesWritten()`, and the concrete type behind the interface (`net/http`'s unexported `*response`) is not something you can type-assert to and inspect. So an access-logging or metrics layer that wants a line like `GET /orders/42 200 1731 3.1ms` has to observe the calls as they happen, which means standing between the handler and the real writer. ## The wrapper The idiomatic shape is a struct that **embeds the interface** and overrides only the methods it needs to observe: - Embedding `http.ResponseWriter` as an anonymous field gives the struct all three methods for free, forwarding to whatever was passed in. - Overriding `WriteHeader` stores the code and then calls the embedded writer's `WriteHeader`, so the response still goes out exactly as before. Forgetting to forward is the classic beginner bug: the handler then produces no response at all. - Overriding `Write` calls the embedded writer's `Write`, adds the returned `n` to a counter, and returns `(n, err)` unchanged. `Write` returns the number of bytes actually written, which is what you want to record; never assume it equals `len(b)`. The middleware then constructs one of these per request and hands **it** to the inner handler: ```go rec := &recorder{ResponseWriter: w, status: http.StatusOK} start := time.Now() next.ServeHTTP(rec, r) log.Printf("%s %s %d %d %s", r.Method, r.URL.Path, rec.status, rec.nbytes, time.Since(start)) ``` The handler sees an `http.ResponseWriter` like any other and cannot tell the difference. Pass the pointer, not a copy of the struct: the overridden methods have pointer receivers so that the writes they record survive, and a copy would leave the middleware reading fields nobody updated. ## What the recorded numbers actually mean - **Status.** The code passed to `WriteHeader`. If the handler never calls it, `net/http` sends `200` implicitly and your override never runs — which is why a recorder is normally seeded with `http.StatusOK` rather than left at the `int` zero value. - **Bytes.** The sum of the `n` values returned by `Write`. That counts **body** bytes only. It does not include the status line, the response headers, chunked-encoding framing, or TLS overhead, so it will not match what a network counter or a load balancer reports. - **Duration.** Measure it in the middleware around `next.ServeHTTP`, not inside the handler. ## Why not something else A few plausible-looking alternatives do not work: - **Reading it off the request.** `http.Request` has a `Response` field, but it is only populated on the *client* side for the response that caused a redirect. It is nil in a server handler. - **Having handlers report their own status.** That works only for code you control, and any handler that forgets — or any `http.ServeMux` 404, `http.Error` call, or third-party handler you import — silently disappears from the logs. The whole point of a wrapper is that it observes every response, including the ones no handler explicitly wrote. - **Returning the status from the handler.** `http.Handler.ServeHTTP` has no return value; changing that means abandoning the standard interface and every piece of code that expects it. ## The cost of standing in the middle Wrapping is cheap but not free of consequences: the value the handler now holds is *your* type, not `net/http`'s, so anything a handler does beyond the three interface methods — asking the writer for an optional capability, or a standard-library helper looking for a faster write path — now sees your struct instead. That is the trade an access-log layer accepts, and it is why the wrapper is usually written once, in one shared place, rather than reinvented per service.
- What exactly does the byte count from an overridden Write cover?Only response body bytes, summed from the `n` that each `Write` returns. It excludes the status line, the response headers, chunked-encoding framing and any TLS or compression overhead below you, so it will not match a load balancer's byte counter. If the wrapper also forwards `ReadFrom`, that path has to add its own count or large file responses log as zero bytes.
- What should the wrapper do if a handler calls WriteHeader twice?Record the first final code and forward every call unchanged. `net/http` ignores the second one and logs a superfluous `WriteHeader` call warning to `Server.ErrorLog`, so a recorder that keeps the last value it saw would log a status the client never received. A boolean latch on the wrapper keeps the log honest without changing what goes on the wire.
- Why pass a pointer to the wrapper rather than the struct value?The overrides are declared on the pointer type so their assignments to `status` and `nbytes` persist; handing over a copy of the struct would give the handler a value whose recorded fields the middleware never sees. Passing `&rec` also keeps the per-request allocation to one object, which matters when the layer runs on every request.
It is a postal drop slot, not a mailbox: you can push letters through it, but you cannot open it later to see what went out. If you want a record, you steam open each envelope on the way in.
saying these in an interview costs you the question
- Claims you can read the status back off http.ResponseWriter
- Looks for the status on http.Request.Response in a handler
- Overrides WriteHeader but forgets to forward the call
- Logs the status before calling next.ServeHTTP
- Assumes Write always writes len(b) bytes
- Passes the wrapper struct by value and reads empty fields