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.