Making Life Easier With Middleware

Patrick Hahn, Marvin Wendt and Lars Hick, 18 November 2022

Many developers are familiar with code duplication. Particularly, but not exclusively, in the area of web services, recurring tasks such as authentication, recording of metrics, logging and many more. In order not to have to implement these tasks anew each time and, above all, to avoid redundant code, the middleware pattern exists. This article first gives a general overview of how middleware works. Then the pattern is explained using concrete examples written in Go.

What is Middleware?

As already quoted, middleware is a pattern to prevent code duplication. Figuratively speaking, middleware is a piece of software that is plugged into the code execution chain and automatically executed for defined use cases without the need to be called every now and then. Middleware not only is a phenomenon of web service development; however, the following examples will be provided in that context.

Types of middleware

In general, one can differentiate between three types of middleware to fulfil different scenarios. Here is a short overview of them with some examples:

Pre-attached Middleware:

This type of middleware is run in the beginning of the code execution chain. Pre-attached middleware is very useful when it comes to authentication. Executing code and processing an API request which is meant to be secured does not make sense, unless the request is authenticated. Therefore, it should first be checked whether the requester has the right to make this request. This is, where pre-attached middleware comes into place.

Post-attached Middleware:

In case one would like to log something, whenever an API request has completed (no matter if successful or not), post-attached middleware does the job. In opposite of pre-attached middleware, it is being executed after the rest of the code, that refers to the API call.

Wrapping Middleware

A special position is occupied by this type. It is a hybrid of the two previous ones shown. Logging is only useful if there is relevant information that can be logged. To improve the information being logged in the example of the post-attached middleware , wrapped middleware could be used to add information about processing time of the request. Therefore, a timestamp of when the request was registered is added before anything of the correlating logic is executed (pre-attached middleware). Once the request has been processed, this information finally can be grabbed and logged (post-attached middleware). So, wrapping middleware is a combination of pre-attached and post-attached middleware. Keep in mind, that all of this would have to be done for each API endpoint separately. It would produce a lot of redundant (and sometimes also error-prone) code if the middleware pattern wasn’t utilized.

Middleware in Go

Let us now move on to the practical part. We have already had some examples of how middleware can be useful. We will now go into some of them in more detail with Go code.

General example

Middleware functions often get used together with the net/http HTTP server. However, the pattern itself is independent of net/http or even HTTP serving in general. Let’s consider an imaginary event bus, that calls a single handler when an event is sent to it.

package eventbus

// imports omitted

// Event is an envelope type for all kinds of event data.
type Event struct {
  Type      string
  Timestamp time.Time
  Data      any
}

// Handler is an interface that must be satisfied by any 
// component that handles events.
type Handler interface {
  HandleEvent(event Event)
}

// Our example event bus.
// It supports only one handler per type.
type EventBus struct {
  handlers map[string]Handler
}

func New() *EventBus {
  return &EventBus{
    handlers: map[string]Handler{},
  }
}

// On registers a handler for a specific event type with 
// the event bus
func (e *EventBus) On(eventType string, handler Handler) {
  e.handlers[eventType] = handler
}

// Send submits an event to the event bus, to be handled 
// by the previously registered handler
func (e *EventBus) Send(event Event) {
  handler, ok := e.handlers[event.Type]
  if !ok || handler == nil {
    // If we didn't find an appropriate handler, exit early
    return
  }
  
  // Let the handler take care of this event
  handler.HandleEvent(event)
}

First login of a user

We can use this event bus then like this:

package main

// imports omitted

// FirstLoginHandler is an event handler that reacts to 
// a user logging in for the first time. 
// We want to greet them via mail, so we send out a 
// welcome mail via the fictional type "mail.Sender"
type FirstLoginHandler struct {
  mail mail.Sender
}

// FirstLoginData is an event payload that identifies 
// the user who just completed their first log in.
type FirstLoginData struct {
  Username string
  Email    string
}

func (f *FirstLoginHandler) HandleEvent(event eventbus.Event) {
  // Get the FirstLoginData payload out of the event
  data, ok := event.Data.(FirstLoginData)
  if !ok {
    // If the event does not contain FirstLoginData, exit early
    return 
  }
  // Send the welcome mail
  f.mail.Send(
    data.Email, 
    fmt.Sprintf("Hello %s, welcome to our product!", 
      data.Username,
    ),
  )
}
func main() {
  // Let's simulate the case that someone 
  // signed in for the first time
  mail := mail.NewSender( /*...*/)
  eventBus := eventbus.New()
  eventBus.On("user.firstlogin", FirstLoginHandler{mail: mail})
}

We create a new struct FirstLoginHandler that implements the Handler interface of the event bus. On a user’s first login, this handler sends an email to the user, welcoming them to our product. Now, to get additional observability into our first user handling, we want to add logging to this. In theory, we could just add the log statement into our FirstLoginHandler but doing so would clutter it. The focus of this handler should be sending that email, not adding observability.

Logging Middleware

To tackle this issue, we can write another handler, that puts itself between the event bus and logs everything that passes through it – with data on debug level, and without data on info level. In the end, the logger middleware itself calls the next handler in line. This could be again another middleware, or the real handler finally.

package logging

// LoggerMiddleware is our first example of a middleware. 
// It implements the Handler interface just like 
// a normal event handler, but it logs the event and 
// forwards it to another handler (Next)
type LoggerMiddleware struct {
  Logger zap.SugaredLogger
  Next   eventbus.Handler
}

func (l *LoggerMiddleware) HandleEvent(event eventbus.Event) {
  l.Logger.Debugw("event data", "type", event.Type, "time",
    event.Timestamp, "data", event.Data)
  l.Logger.Infow("event", "type", event.Type, 
    "time", event.Timestamp)
  
  // Here we call out to the next handler, our logging happens 
  // before the event is actually handled.
  l.Next.HandleEvent(event)
}

We would use this middleware like so:

package main
// see previous examples
func main() {
  mail := mail.NewSender( /*...*/)
  eventBus := eventbus.New()
  
  // Instead of registering FirstLoginHandler directly, 
  // we wrap it in a LoggingMiddleware
  eventBus.On("user.firstlogin", logging.LoggingMiddleware{
    Logger: sugaredLogger,
    Next:   FirstLoginHandler{mail: mail},
  })
}

Why should we let the middleware call the next handler in line? Wouldn’t it be better to let the event bus handle calling all the handlers in order? The benefit of this approach is that it lets us control when and even if the next handler is called. The event bus has just a simple map, that maps event type to a specific handler that should handle the event. You can register events with the ‘On’ function and send events into the bus to be distributed by calling the ‘Send’ function.

Logging middleware

Introducing middleware with net/http

When using net/http, we have some similarities to the example above: Our events are now HTTP requests that need to be handled, instead of the eventbus.Handler interface, we now have the http.Handler interface, and the event bus gets replaced by some implementation of a muxer, like the build-in http.ServeMux. One difference here is, that we can add middleware at various levels: Between the http.Server and http.ServeMux, between ServeMux and the handlers, or between different levels of ServeMuxes. The following shows a minimal HTTP server that serves a “Hello World” endpoint, where you can pass in a replacement for “World” through the name query parameter.

package main

import (
  "fmt"
  "net/http"
)

func main() {
  mux := http.NewServeMux()
  
  // Register our example API endpoint: 
  // A personalized Hello-World API
  mux.HandleFunc("/hello-world", func(
    writer http.ResponseWriter, 
    req *http.Request,
  ) {
    // Get the name from the query parameter
    name := req.URL.Query().Get("name")
  
    // Use "World" as default if no name was given
    if name == "" {
      name = "World"
    }
  
    // Respond with the personalized Hello World
    _, _ = writer.Write([]byte(fmt.Sprintf("Hello %s", name)))
  })
  
  // Configure and start the HTTP server
  server := http.Server{
    Handler: mux,
    Address: ":8080",
  }
  err := server.ListenAndServe()
  if err != nil {
    panic(err)
  }
}

No middleware

Logging

We now want to add logging to either this endpoint or to all requests towards the server. We can do this by writing a logging middleware, just as above. We want to log incoming requests, but also responses once the request has been handled. This way, we can also get additional data such as response codes or response body sizes for our log. This example shows how we can replace the ‘http.ResponseWriter’ that gets passed into the middleware with our own instance to get this kind of information from the downstream handler.

package logging

import "net/http"

// Our Logging Middleware. Just as before, 
// we need a Logger and the handler that 
// should be next in line
type LoggerMiddleware struct {
  Logger zap.SugaredLogger
  Next   http.Handler
}

// ResponseWriterWrapper is a wrapper around the 
// default http.ResponseWriter that is given into 
// net/http handlers. It intercepts the WriteHeader 
// call and saves the response status code into the 
// attribute "WrittenResponseCode" for use in our 
// middleware
type ResponseWriterWrapper struct {
  WrittenResponseCode int
  ResponseWriter      http.ResponseWriter
}

// Header method, is just passed through the 
// underlying ResponseWriter
func (rww *ResponseWriterWrapper) Header() http.Header {
  return rww.ResponseWriter.Header()
}

// Write method, is just passed through the 
// underlying ResponseWriter
func (rww *ResponseWriterWrapper) Write(b []byte) (int, error) {
  return rww.ResponseWriter.Write(b)
}

// WriteHeader is used to write out all the HTTP headers
// in our response and set the status code. We are 
// interested in this status code for our middleware, 
// so we save it into the "WrittenResponseCode" attribute.
func (rww *ResponseWriterWrapper) WriteHeader(status int) {
  rww.WrittenResponseCode = status
  rww.ResponseWriter.WriteHeader(status)
}

// Our HTTP middleware handler method. It satisfies
// the http.Handler interface, but does not actually 
// handle the request itself, but forwards it to the 
// next handler and logs request and response details.
func (l *LoggerMiddleware) ServeHTTP(
  w http.ResponseWriter, 
  r *http.Request,
) {
  // Log out all the pre-request handling information
  l.Logger.Infow(
    "request",
    "method", r.Method,
    "protocol", r.Proto,
    "path", r.URL.Path,
    "params", r.URL.RawQuery,
    "remote", r.RemoteAddr,
  )
  rww := &ResponseWriterWrapper{
    ResponseWriter: w,
  }
  
  // Call the next http handler 
  // with a wrapped http ResponseWriter
  l.Next.ServeHTTP(rww, r)
  
  // Log out all the post-request handling information
  l.Logger.Infow(
    "request",
    "method", r.Method,
    "protocol", r.Proto,
    "path", r.URL.Path,
    "params", r.URL.RawQuery,
    "remote", r.RemoteAddr,
    "status", rww.WrittenResponseCode,
  )
}

This handler can be used in multiple ways: We can register it between ServeMux, or between ServeMux and our own handler. The first option will apply this middleware to all requests to this server, the second option only for this single endpoint.

package main

import (
  "fmt"
  "net/http"
)

// Our Hello World API handler from before, 
// but now it is a standalone function
func helloWorldHandler(
  writer http.ResponseWriter, 
  req *http.Request,
) {
  name := req.URL.Query().Get("name")
  if name == "" {
    name = "World"
  }
  _, _ = writer.Write([]byte(fmt.Sprintf("Hello %s", name)))
}

func main() {
  // Initialize our logger
  logger, _ := zap.NewProduction()
  defer logger.Flush()
  
  mux := http.NewServeMux()
  
  // 1. Option: Register our middleware between 
  // ServeMux and handler implementation
  // This way, we get all Hello World API requests
  mux.Handle("/hello-world", logging.LoggerMiddleware{
    Logger: logger.Sugar(),
    Next:   http.HandlerFunc(helloWorldHandler),
  })
  
  // 2. Option: Register our middleware between 
  // Server and ServeMux.
  // This way get get all requests for this server
  server := http.Server{
    Handler: logging.LoggerMiddleware{
      Logger: logger.Sugar(), 
      Next: mux,
    },
    Address: ":8080",
  }
  err := server.ListenAndServe()
  if err != nil {
    panic(err)
  }
}

Authentication Middleware

Another use-case for middlewares is fetching authentication information from the request, thereby centralizing the place where this information gets processed. In this example we will use Basic Authentication, as this is really easy to implement. After we have extracted the authentication information from the request, we need to pass this information to the handler in some way. There is an implicit and explicit way: We can either use the request context to pass this information, or we can extend the method signature to include user information. Passing via context means the method signature of the context stays intact, and we can layer more middleware in-between the authentication check that might need authentication beforehand. Passing via parameter means the handler makes its dependency on user information explicit, and it can’t ever be accidentally used without authentication middleware. We will show both examples, you have to decide this trade-off for yourself.

Via Context
package authentication

import (
  "context"
  "errors"
  "fmt"
  "net/http"
)

type userKey string

var (
  ErrUserNotInContext = errors.New("no user entry found in context")
)

// UserService is a fictional part of our
// application that is used to interface 
// with our user management. The implementation 
// of that is not part of this article
type UserService interface {
  FetchUser(username, password string) (*User, error)
}

// BasicAuthMiddleware is an HTTP middleware 
// implementation that requires users to 
// authenticate themselves via HTTP Basic 
// Authentication
type BasicAuthMiddleware struct {
  UserService UserService
  Realm       string
  Next        http.Handler
}

func (b *BasicAuthMiddleware) ServeHTTP(
  w http.ResponseWriter, 
  r *http.Request,
) {
  // Get the username and password from the request
  reqUsername, reqPassword, ok := r.BasicAuth()
  if !ok {
    // No basic auth credentials present
    // Write out the response that will 
    // request this from the browser
    w.Header.Set("WWW-Authenticate", 
      fmt.Sprintf("Basic realm=%s,charset = %q", 
        b.Realm, "UTF-8"))
    w.WriteHeader(401)
    _, _ = w.Write([]byte("Unauthenticated"))
    return
  }
  
  // Get the user record from the user management
  user, err := b.UserService.FetchUser(reqUsername, reqPassword)
  if err != nil {
    // Wrong basic auth credentials present
    // Request them again from the browser or client
    w.Header.Set("WWW-Authenticate", 
      fmt.Sprintf("Basic realm=%s,charset = %q",
        b.Realm, "UTF - 8"))
    w.WriteHeader(401)
    w.Write([]byte("Unauthenticated"))
    return
  }
  
  // Add the user record to our http 
  // context and request
  authenticatedCtx := context.WithValue(r.Context(), userKey("user"), user)
  authenticatedReq := r.Clone(authenticatedCtx)
  
  // Call the next handler with our 
  // new request that contains the 
  // context with the user record
  b.Next.ServeHTTP(w, authenticatedReq)
}

// GetUser is a way for our handlers to 
// easily get the user record back from 
// the context again
func GetUser(ctx context.Context) (*User, error) {
  val := ctx.Value(userKey("user"))
  if val == nil {
    return nil, ErrUserNotInContext
  }
  user, ok := val.(*User)
  if !ok {
    return nil, ErrUserNotInContext
  }
  return user, nil
}

This middleware extracts basic auth credentials, verifies them with a user service (out of scope here), reacts appropriately when the credentials are not present or invalid, and modifies the context to include the User struct. The GetUser function also provides a way to get a User (or error) back from the context, for the handler to use.

Context Auth Middleware Sequence Diagram

Via Parameter
package authentication

import (
  "fmt"
  "net/http"
)

// Authenticated variant of http.Handler
type AuthenticatedHandler interface {
  ServeHTTP(http.ResponseWriter, *http.Request, *User)
}

// Authenticated variant of http.HandlerFunc
type AuthenticatedHandlerFunc func(http.ResponseWriter, *http.Request, *User)

// This is an example of the "functional interfaces" pattern
func (h AuthenticatedHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, u *User) {
  h(w, r, u)
}

// Fictional user management component. See example above.
type UserService interface {
  FetchUser(username, password string) (*User, error)
}

// Our Basic Auth HTTP middleware. This 
// time, the Next handler is not a http.Handler, 
// but an AuthenticatedHandler: our own interface 
// for HTTP handlers that require authentication 
// information
type BasicAuthMiddleware struct {
  UserService UserService
  Realm       string
  Next        AuthenticatedHandler
}

func (b *BasicAuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  reqUsername, reqPassword, ok := r.BasicAuth()
  if !ok {
    // No basic auth credentials present
    w.Header.Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%s,charset = %q", b.Realm, "UTF - 8"))
    w.WriteHeader(401)
    w.Write([]byte("Unauthenticated"))
    return
  }
  user, err := b.UserService.FetchUser(reqUsername, reqPassword)
  if err != nil {
    // Wrong basic auth credentials present
    w.Header.Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%s,charset = %q", b.Realm, "UTF-8"))
    w.WriteHeader(401)
    w.Write([]byte("Unauthenticated"))
    return
  }
  
  // Call the next http handler, 
  // but with authentication information
  b.Next.ServeHTTP(w, r, user)
}

In this implementation we need our own interface that the authenticated handlers can implement. For quality of life, we can provide the same possibilities as the http package does, http.Handler as an interface, and http.HandlerFunc to make standalone functions conform to the interface. Instead of the normal http.Handler, that usually gets used as the Next field, we take our own interface here. In the function itself, we just pass the user that we get from the service to the next handler.

Metrics

Adding metrics about served requests is a good way to get more insight into the runtime of your application once it actually runs in production. In this case, we will only show an example of how this could be implemented, for actually adding metrics in production, please refer to the next section “Ready-made middleware”.

package metrics

import (
  "net/http"
  "time"
)

var (
  // Our Prometheus metric that we want to export
  httpServerRequestDurHist = prometheus.NewHistogramVect(
    prometheus.HistogramOpts{
      Name: "http_server_request_duration_seconds",
      Help: "Histogram about request processing latencies",
    },
    []string{"code", "method"},
  )
)

type PrometheusMiddleware struct {
  Next http.Handler
}

func (p *PrometheusMiddleware) ServeHTTP(
  w http.ResponseWriter, 
  r *http.Request,
) {
  // Before handling the API call: Record 
  // the current time to calculate the 
  // duration later
  start := time.Now()
  // Wrapping the ResponseWriter again to get 
  // the HTTP response code our API returns
  rww := &ResponseWriterWrapper{
    ResponseWriter: w,
  }
  p.Next.ServeHTTP(rww, r)
  
  // After handling the API call: Add the 
  // duration to the Prometheus histogram
  httpServerRequestDurHist.With(prometheus.Labels{
    "code":   rww.ResponseCode,
    "method": r.Method,
  }).Observe(time.Since(start).Seconds())
}

Functional Middleware

As you may have noticed in the Basic Auth middleware example, net/http offers an additional way of implementing request handlers: http.HandlerFunc instead of http.Handler. How does this work? Under the hood, the http.HandlerFunc type also implements the http.Handler interface, by just delegating calls to ServeHTTP to itself. This way, we can also implement middleware in a functional style, instead of the OOP style that has been used here before.

package metrics

import (
  "net/http"
  "time"
)

// PrometheusMiddleware is a functional 
// implementation of the same Prometheus HTTP 
// middleware as before
func PrometheusMiddleware(
  latencyHistogram prometheus.HistogramVec, 
  next http.Handler,
) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    // Wrapping the ResponseWriter again to get 
    // the HTTP response code our API returns
    rww := &ResponseWriterWrapper{
      ResponseWriter: w,
    }
    next.ServeHTTP(rww, r)
    latencyHistogram.With(prometheus.Labels{
      "code":   rww.ResponseCode,
      "method": r.Method,
    }).Observe(time.Since(start).Seconds())
  }
}
package main

func main() {
  // ...
  // Installing the middleware is as simple as 
  // calling the PrometheusMiddleware function 
  // with the histogram and the next handler
  mux.Handle("/hello-world", PrometheusMiddleware(
    httpServerRequestDurationHistogram,
    helloWorldHandler),
  )
  // ...
}

Ready-made middleware

The gorilla library provides middlewares that solve common use-cases like logging, compression or method matching in the GitHub Repo gorilla/handlers.

The Prometheus Go client library provides HTTP middleware for exporting Prometheus metrics about the handled request, like currently in flight requests, counter of handled requests, response time histograms, request and response sizes, and more. You can find these in the subpackage prometheus/promhttp in the GitHub repository prometheus/client_golang.

The InstrumentHandlerDuration middleware can automatically provide you all the metrics that you need to implement RED (Requests, Error, Duration) metrics for your service. It can automatically fill in response code and request method, but for more information, like the request path, you can provide the information to Prometheus yourself.

Recap

At the start of this article, we introduced middleware as a way to implement cross-cutting concerns separately from our business logic. We then implemented a middleware to introduce logging to our fictional event bus and registered our handler with the middleware. After the event bus, we moved on to a more realistic example with Go’s net/http package, implementing logging, metrics export and HTTP basic authentication without modifying our API logic. In the latter example, we have shown that there are multiple possibilities to pass on information from the middleware further down the chain, each with their own up- and downsides. And at last, we referred to already existing middleware that you can use to easily add functionality to your API.