Request Logging
The previous lesson wrapped every handler in recovery middleware, using the
func(http.Handler) http.Handler shape from the Fundamentals middleware
lesson. This one wraps every handler again, for a different reason: an access
log. Every request that reaches the server should leave a trace of what
method and path it hit and what status came back. The mechanics of wrapping
next are the same as before — what's new is a problem recovery middleware
never had to face.
The problem: ResponseWriter doesn't remember
http.ResponseWriter is write-only. A handler calls WriteHeader(201) or
just Write(body), and the interface has no method to ask afterward "what
status did you send?" That's fine for the handler itself — it knows what it
wrote — but it's a problem for middleware sitting outside the handler,
which only gets w and r and needs to log the outcome after next has
already run and returned.
Extending an interface by embedding it
The fix is a small struct that embeds http.ResponseWriter:
type statusRecorder struct {
http.ResponseWriter
status int
}
Embedding the interface means statusRecorder automatically satisfies
http.ResponseWriter itself — every method the interface requires
(Header, Write, WriteHeader) is promoted from the embedded value for
free. You get a working ResponseWriter without writing three method
bodies. The only one worth overriding is WriteHeader:
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
This shadows the promoted method. Calling rec.WriteHeader(201) now runs
this function instead of the embedded one — it records code in the
struct field, then explicitly delegates to r.ResponseWriter.WriteHeader
so the real header still gets sent. Skip that last line and the response
would never actually report its status to the client, only to your struct.
Defaulting to 200
Not every handler calls WriteHeader explicitly. Calling Write with no
prior WriteHeader call implicitly sends 200 OK — that's how
net/http behaves, and it means statusRecorder has to assume 200 up
front rather than leaving status at its zero value:
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
If a handler never calls WriteHeader, rec.status stays 200, matching
what the client actually receives. If it does call WriteHeader, the
override above updates it before the real header goes out.
Logging after next runs
The middleware passes rec, not w, down the chain, so every write the
handler makes passes through the wrapper first:
func LogRequests(next http.Handler, out io.Writer) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rec, r)
fmt.Fprintf(out, "%s %s %d", r.Method, r.URL.Path, rec.status)
})
}
The log line is written after next.ServeHTTP returns, once the handler
has finished and rec.status holds whatever it actually sent. Writing
before that point would only ever log the default.
Where this goes next
fmt.Fprintf to an io.Writer is enough to prove the mechanism, and it's
what keeps this exercise testable without a real server. Production code
usually swaps it for structured logging — key-value fields, log levels,
JSON output — via log/slog, which Part 4 covers on its own. The
statusRecorder pattern doesn't change; only what you do with the captured
status does.
Practice
1. Request Logging
IntermediateAccess logs need the response status — but a plain
http.ResponseWritergives you no way to read back what a handler wrote. The fix is a tiny wrapper.- Define
statusRecorderembeddinghttp.ResponseWriterwith anint statusfield, and overrideWriteHeader(code int)to recordcodebefore delegating to the embedded writer. - Implement
LogRequests(next http.Handler, out io.Writer) http.Handler: wrapwin astatusRecorder(default status200), callnextwith the wrapper, then write"METHOD PATH STATUS"toout(e.g."POST /tasks 201").
Because you pass the wrapper to
next, itsWriteHeadercalls land on your recorder — so you can read.statusafterward.package main import ( "io" "net/http" ) // TODO: wrap w in a statusRecorder defaulting to status 200, call next with // the wrapper, then write "METHOD PATH STATUS" to out (e.g. "POST /tasks 201"). func LogRequests(next http.Handler, out io.Writer) http.Handler { return next } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Define