SyntaxStudy
Sign Up
Go Middleware and Handler Chains
Go Beginner 1 min read

Middleware and Handler Chains

Middleware is a handler that wraps another handler to add cross-cutting functionality such as logging, authentication, rate limiting, or request tracing. In Go, middleware is implemented as a function that takes an `http.Handler` and returns an `http.Handler`. This allows middleware to be composed in a chain: each layer adds behaviour and then calls the next handler. Because middleware is just a function transformation over `http.Handler`, chains can be built with straightforward function composition. A middleware can execute code before the next handler (preprocessing: reading headers, authenticating), after it (postprocessing: logging the response status), or both by wrapping the call to the next handler. The `http.ResponseWriter` interface does not expose the status code after `WriteHeader` is called, which complicates logging. A common solution is a responseRecorder wrapper that intercepts `WriteHeader` and `Write` calls to capture the status code. This is the same approach taken by `httptest.ResponseRecorder` in the testing package.
Example
package main

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

// responseRecorder wraps ResponseWriter to capture the status code.
type responseRecorder struct {
    http.ResponseWriter
    status int
}

func (rr *responseRecorder) WriteHeader(code int) {
    rr.status = code
    rr.ResponseWriter.WriteHeader(code)
}

// loggingMiddleware logs method, path, status, and duration.
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rr    := &responseRecorder{ResponseWriter: w, status: http.StatusOK}
        next.ServeHTTP(rr, r)
        log.Printf("%s %s %d %v", r.Method, r.URL.Path, rr.status, time.Since(start))
    })
}

// authMiddleware rejects requests without an API key header.
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("X-API-Key") != "secret" {
            http.Error(w, "unauthorised", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func chain(h http.Handler, mws ...func(http.Handler) http.Handler) http.Handler {
    for i := len(mws) - 1; i >= 0; i-- {
        h = mws[i](h)
    }
    return h
}

func main() {
    hello := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, authenticated user!")
    })

    mux := http.NewServeMux()
    mux.Handle("/secure", chain(hello, loggingMiddleware, authMiddleware))
    mux.Handle("/public", loggingMiddleware(
        http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            fmt.Fprintln(w, "Hello, public!")
        }),
    ))

    log.Println("listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}