Go: Wrapping http.ResponseWriter with Middleware
June 6, 2017
Using middleware provides a clean way to reduce code duplication when handling HTTP requests in Go. By utilizing the standard handler signature, func(w http.ResponseWriter, r *http.Request)
we can write functions that are easily dropped into any Go web service. One of the most common uses of middleware is to provide request/response logging. In order to log responses we will need to capture the status code that was written by a nested handler.
Since we don’t have direct access to this status code, we will wrap the ResponseWriter
that is passed down:
type statusRecorder struct {
http.ResponseWriter
status int
}
func (rec *statusRecorder) WriteHeader(code int) {
rec.status = code
rec.ResponseWriter.WriteHeader(code)
}
func logware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Initialize the status to 200 in case WriteHeader is not called
rec := statusRecorder{w, 200}
next.ServeHTTP(&rec, r)
log.Printf("response status: %v\n", rec.status)
})
}
Because the
http.ResponseWriter
type is an interface we can pass a custom type to subsequent handlers as long as it implements all of the defined methods:
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(int)
}
By using composition we only need to explicitly implement the WriteHeader
method that we want to modify. This is because we have embedded the ResponseWriter
type in our struct so all of its methods get promoted and can be called on statusRecorder
.
Note: We pass a pointer into ServeHTTP
so that nested handlers can change the status
field that we later reference.
Using the middleware is simple:
func main() {
http.ListenAndServe(":8080",
logware(
http.HandlerFunc(handle),
),
)
}
func handle(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(201)
w.Write([]byte("Accepted"))
}
The above handle
function calls two methods:
WriteHeader
: This calls ourstatusRecorder.WriteHeader
method which records the201
status for logging and then calls the originalResponseWriter.WriteHeader
method.Write
: This calls the originalResponseWriter.Write
method directly.
Why do we choose to store the status in our struct rather than having the WriteHeader
method do the logging? Calling WriteHeader
inside of a handler is optional. An implicit 200-OK status is sent to the client if the status code is not set. In this situation the logging call would be missed.
Happy coding, and be on the lookout for other places to use composition and interfaces to simplify your Go code.