Hi, I'm Tim

I like to dabble in all things related to programming, and occasionally I write about what I’m up to.

[Go] Capturing the HTTP status code from http.ResponseWriter

I’ve slowly but surely been learning Go over the past few months, and I recently began working on a web service in my spare time. Go comes with a great builtin package for building web services, net/http, and in this post I’ll cover how to wrap the http.ResponseWriter type to capture the HTTP status code from the response. This is useful if you need to log the status code after the response handler has completed. If you need to keep track of any other part of the response, this approach can easily be extended.

Getting started

To get things started, let’s put together a very basic web service that responds with a 404 Not Found to all requests except requests for the root:

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", handleRoot)

    log.Fatal(http.ListenAndServe(":8080", nil))
}

func handleRoot(w http.ResponseWriter, req *http.Request) {
    if req.URL.Path != "/" {
        http.NotFound(w, req)
        return
    }

    fmt.Fprintf(w, "Hello, World!")
}

After starting our service using go run main.go, navigating to http://localhost:8080/ should present us with "Hello, World!". If we navigate to any other path, we should get a 404.

Before we continue, take a moment to appreciate how easy it was to write that. We didn’t even have to install a gem or a pod or an egg!

Adding logging

Now, to make things more interesting, we decide that we should log all requests. Our first idea is to create a function that takes a http.Handler and wraps it, returning a new handler that logs some information about the request before invoking the wrapped handler:

func wrapHandlerWithLogging(wrappedHandler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        log.Printf("--> %s %s", req.Method, req.URL.Path)

        wrapperHandler.ServeHTTP(w, req)
    })
}
func main() {
    rootHandler := wrapHandlerWithLogging(http.HandlerFunc(handleRoot))
    http.Handle("/", rootHandler)

    log.Fatal(http.ListenAndServe(":8080", nil))
}

This works just as one would expect. If we make a few requests to our server, we will see something along the following lines in our terminal:

$ go run main.go
2016/03/02 21:44:42 --> GET /
2016/03/02 21:46:18 --> GET /foo
2016/03/02 21:46:20 --> GET /bar
2016/03/02 21:46:21 --> GET /baz

Pretty cool! However, it would be even cooler if we could log the response as well. We figure we can just add another log.Printf after the call to wrappedHandler.ServeHTTP to log the things we are interested in. An important and informative part of the response is the HTTP status code, so we look through the documentation for http.ResponseWriter to find out how to retrieve the status code. But wait… we quickly realize that http.ResponseWriter doesn’t expose the status code! Will this be where our adventure with net.http ends?

Of course not! After a closer look, we see that ResponseWriter.WriteHeader takes the status code as an argument. If we could somehow intercept that method, then we could store the status code and use it for logging later. This is in fact possible, and quite easy to do at that. By creating a new struct that embeds an http.ResponseWriter we can implement the WriteHeader method to store the status code in our new struct:

type loggingResponseWriter struct {
    http.ResponseWriter
    statusCode int
}

func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
    return &loggingResponseWriter{w, http.StatusOK}
}

func (lrw *loggingResponseWriter) WriteHeader(code int) {
    lrw.statusCode = code
    lrw.ResponseWriter.WriteHeader(code)
}

When we have our new loggingResponseWriter type, we simply need to make sure that our wrapped handler is passed a loggingResponseWriter instead of the standard http.ResponseWriter:

func wrapHandlerWithLogging(wrappedHandler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        log.Printf("--> %s %s", req.Method, req.URL.Path)

        lrw := NewLoggingResponseWriter(w)
        wrappedHandler.ServeHTTP(lrw, req)

        statusCode := lrw.statusCode
        log.Printf("<-- %d %s", statusCode, http.StatusText(statusCode))
    })
}

If we run our service again and make some requests we will see that the HTTP status code from the response is now logged:

$ go run main.go
2016/03/03 20:17:53 --> GET /
2016/03/03 20:17:53 <-- 200 OK
2016/03/03 20:17:56 --> GET /foo
2016/03/03 20:17:56 <-- 404 Not Found
2016/03/03 20:17:57 --> GET /bar
2016/03/03 20:17:57 <-- 404 Not Found

Just what we wanted!

Summary

When I began working on this problem I was afraid that the code required to solve the problem would be complex and difficult to understand. However, it turns out that capturing the HTTP status code (or any other data, for that matter) from the response is quite simple and easy to reason about.

The approach discussed in this post gives a brief glimpse into how powerful wrapping an http.Handler can be by making it easy to add functionality on top of existing handlers. Extending this approach to things like authorization is a natural next step, and combining it with x/net/context makes it even more powerful.

If you want to try out the code with minimal effort I’ve created a gist with the entirety of the code outlined in this post. It also includes some small comments about minor details that might not be entirely obvious from the post.

Until next time!