Go http middleware chain with context package

Middleware is a function which wrap http.Handler to do pre or post processing of the request.

Chain of middleware is popular pattern in handling http requests in go languge. Using a chain we can:

  • Log application requests
  • Rate limit requests
  • Set HTTP security headers
  • and more

Go context package help to setup communication between middleware handlers.

Implementation

Middleware chain

To compose handlers into a chain I choose Alice package which implement logic of looping through middleware functions.

chain := alice.New(security.SecureHTTP, httpRateLimiter.RateLimit, myLog.Log).Then(myAppHandler)    

http.ListenAndServe(":8080", chain) 

All source code is available at GitLab.

The only requirement of Alice that handlers should follow:

func (http.Handler) http.Handler

Example of SecureHTTP handler:

func (security *SecurityHTTPOptions) SecureHTTP(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		for header, value := range security.headers {
			w.Header().Add(header, value)
		}

		h.ServeHTTP(w, r)
	})
}

Context – communicator between middleware calls

Using go context package we can pass objects between middleware functions with:

  • context.WithValue
  • http.Request.WithContext
  • http.Request.Context().Value()
func (this *Logger) Log(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := context.WithValue(r.Context(), LoggerContextKey, this)
		t1 := time.Now()
                h.ServeHTTP(w, r.WithContext(ctx))
		t2 := time.Now()
		this.Printf("%s %q %v\n", r.Method, r.URL.String(), t2.Sub(t1))
	})
}

Where LoggerContextKey is a key by which we can use later to extract logger object:

func Recover(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				mylog := r.Context().Value(logger.LoggerContextKey).(*logger.Logger)
				mylog.Printf("panic: %+v", err)
				http.Error(w, "500 - Internal Server Error", http.StatusInternalServerError)
			}
		}()

		h.ServeHTTP(w, r)
	})
}

Constructors

Sometimes you need to initialize objects before they are ready to use. To go around Alice limitation on func signature we can make a wrapper type for a particular package. Let see on log package example:

Wrapper type

type Logger struct {
	*log.Logger
}

Constructor

func InitLogger() *Logger {
	return &Logger{log.New(os.Stdout, "logger: ", log.Lmicroseconds)}
}

In the main program we use it like:

myLog := logger.InitLogger()

Conclusions

  • usually middleware functions have to be small and fast, so they don’t impact on overall application performance
  • chain is just a simple loop. You can write your own to support customized order of calls
  • create new custom type when you need to adjust other libraries to be useful in middleware pattern

You can find full demo of middleware chains at GitLab.