Middleware

Middleware is a function that takes a handler and returns a handler, so logging, auth and panic recovery are written once.

Syntaxfunc middleware(next http.Handler) http.Handler

Some work belongs on every request: writing a log line, checking a token, catching a panic, setting a security header. Middleware is how that work is attached once instead of being pasted into every handler.

Middleware is a function that takes a handler and returns a new handler. The returned handler does its own work, then calls the handler it was given. The whole shape is func(next http.Handler) http.Handler. It can act before the inner handler, after it, or both, and it can decide not to call it at all, which is how an auth check rejects a request.

Ordering

Wrapping nests. In logging(recovery(mux)), logging runs first on the way in and last on the way out, and recovery sits closer to the handler. Recovery goes outside anything that might panic. Request logging usually goes outermost, so it still records requests that a later layer rejected.

Longhand and shorthand

The handler being returned can be a named type with a ServeHTTP method. Almost nobody writes that. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ... }) is the same thing as one expression: the conversion attaches the required method to the function. Both forms produce a value satisfying http.Handler.

What trips people up

To record the status code, w has to be replaced by your own value that remembers it. That wrapper hides the extra abilities of the real writer, such as flushing a streaming response. Give the wrapper an Unwrap() http.ResponseWriter method; http.NewResponseController, added in Go 1.20, follows it back to the real writer.

Example

Example · go
package main

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

// statusWriter remembers the status code the handler wrote.
type statusWriter struct {
	http.ResponseWriter // embedded: all the other methods come from here
	status int
}

func (s *statusWriter) WriteHeader(code int) {
	s.status = code
	s.ResponseWriter.WriteHeader(code)
}

// Unwrap lets http.NewResponseController reach the real writer (Go 1.20).
func (s *statusWriter) Unwrap() http.ResponseWriter { return s.ResponseWriter }

func logging(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 200 is the status when the handler writes a body and nothing else.
		sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
		next.ServeHTTP(sw, r)
		fmt.Printf("%s %s -> %d\n", r.Method, r.URL.Path, sw.status)
	})
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /ok", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "fine")
	})

	h := logging(mux)
	for _, target := range []string{"/ok", "/missing"} {
		h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", target, nil))
	}
}

// Output:
// GET /ok -> 200
// GET /missing -> 404

When to use it

  • Log every request with its method, path and status code.
  • Reject requests without a valid token before any handler runs.
  • Keep one panicking handler from killing the process.
  • Add security headers to every response from one place.

More examples

Turning a panic into a 500

Without this, a nil map write in one handler closes that connection and logs a stack trace, which is survivable but tells the client nothing. With it, the client gets a 500 and your log gets one line naming the route. http.ErrAbortHandler is re-panicked because the server uses it deliberately to abandon a response without noise.

Example · go
func recovery(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if rec := recover(); rec != nil {
				if rec == http.ErrAbortHandler {
					panic(rec) // the server's own way of dropping a reply
				}
				log.Printf("panic on %s %s: %v", r.Method, r.URL.Path, rec)
				http.Error(w, "internal error", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

Applying several in a readable order

Hand-written nesting reads inside out and gets unreadable at four layers. This helper lets the list read in the order requests travel through it. The reverse loop is the whole trick, and it is worth writing once per project rather than importing a library for it.

Example · go
func chain(h http.Handler, m ...func(http.Handler) http.Handler) http.Handler {
	// Apply in reverse so m[0] ends up the outermost layer.
	for i := len(m) - 1; i >= 0; i-- {
		h = m[i](h)
	}
	return h
}

// In main: requestLog is outermost, recovery is next, mux is innermost.
handler := chain(mux, requestLog, recovery, securityHeaders)
log.Fatal(http.ListenAndServe(":8080", handler))

Middleware on one route, not all of them

Because middleware is just a wrapped handler, it can be applied per route instead of to the whole server. That matters for authentication: a health endpoint a load balancer calls must stay open, while everything under /admin must not. Note the explicit http.HandlerFunc conversion, since Handle takes a handler and not a function.

Example · go
mux := http.NewServeMux()
mux.HandleFunc("GET /health", health) // open to everyone

// requireAuth returns 401 and never calls next when the token is wrong.
mux.Handle("GET /admin", requireAuth(http.HandlerFunc(adminPage)))
mux.Handle("POST /admin/users", requireAuth(http.HandlerFunc(createUser)))

Discussion

  • Be the first to comment on this lesson.