Skip to content

Day 088 — Middleware Chains

Month 4 · Week 1 · ⬅ Day 087 · Day 089 ➡ · Journal index

🎯 Learning Objective

Build composable HTTP middleware with the standard library: the func(http.Handler) http.Handler pattern, a Chain combinator, and the ordering rules that make it predictable.

📚 Topics

  • Middleware type, wrapping, Chain, ordering (outermost-first)
  • RequestID / Logger / Recoverer, capturing status, context values

📖 Reading / Sources

📝 Notes

  • A middleware is a function that wraps a handler and returns a handler: type Middleware func(http.Handler) http.Handler. It can run code before and after next.ServeHTTP, short-circuit, or mutate the request → [[http-handler]].
  • Composition order: with a Chain(A, B, C), A is the outermost layer — it runs first on the way in and last on the way out. Implement Chain by wrapping inside-out (for i := len-1; i>=0; i--) so mws[0] ends up outside.
  • Recoverer must be outer (near the top) so it can catch panics from any inner layer/handler. recover() only catches panics on the same goroutine, so a panic in a goroutine the handler spawned is NOT caught → ties to [[panic-recover]].
  • To capture the status code, wrap ResponseWriter in a small struct that records the code in its WriteHeader. The stdlib writer won't tell you the status after the fact.
  • Pass per-request data via context: r.WithContext(context.WithValue(...)), then next.ServeHTTP(w, r2). Use an unexported key type so keys can't collide across packages → [[context-keys]]. Don't smuggle optional params this way; use it for request-scoped values (request id, auth principal).
  • These are plain net/http middleware — identical shape to chi's middleware package, so they're portable between routers → [[router-choice]].

💻 Code Examples

type Middleware func(http.Handler) http.Handler

// Chain(A, B)(h) == A(B(h)): request flows A -> B -> h.
func Chain(mws ...Middleware) Middleware {
    return func(final http.Handler) http.Handler {
        for i := len(mws) - 1; i >= 0; i-- { // wrap inside-out
            final = mws[i](final)
        }
        return final
    }
}

func Recoverer(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if v := recover(); v != nil { // same-goroutine only
                http.Error(w, "internal server error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

srv := &http.Server{Addr: ":8088", Handler: Chain(RequestID, Logger, Recoverer)(mux)}

Full code: examples/month-04/middleware/main.go · Run: go run ./examples/month-04/middleware

🏋️ Exercises / Practice

Exercise Status Link
Chain ordering + panic Recoverer -> 500 exercises/month-04/week-1/middleware

🐛 Mistakes Made

  • Wrote Chain looping forward, which put mws[0] innermost — the logger then saw the post-recover status, not the original. Looping backward fixed the order.
  • Used a plain string context key and collided with another package's value. Switched to an unexported type ctxKey int.

❓ Open Questions

  • Should Recoverer re-panic after responding (so an upstream test harness sees it), or swallow it? (Production: log + 500; tests sometimes want the panic.)

🧠 Active Recall (answer without looking)

  1. Q: In Chain(A, B, C), which middleware runs first on the way in?
A `A` — the first argument is the outermost layer, so it runs first inbound and last outbound.
  1. Q: Why might a Recoverer fail to catch a panic from a handler?
A `recover()` only catches panics on its **own goroutine**; a panic inside a goroutine the handler launched crashes the process.

🪶 Feynman Reflection

Middleware is gift-wrapping for a handler: each layer adds paper around the box, and a request unwraps from the outside in, then the response wraps back up. A Chain just stacks the wrappers in the order I list them, outermost first.

🕳️ Knowledge Gaps

  • Streaming responses through a wrapped ResponseWriter while preserving http.Flusher (need to forward optional interfaces).

✅ Summary

I can write and compose stdlib middleware, control inbound/outbound order with a Chain, capture status codes, and pass request-scoped values through context safely.

⏭️ Next Steps / Prep for Tomorrow

  • Day 089: JSON request/response helpers — strict decoding and uniform errors.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: feat(examples): composable net/http middleware chain (day 088)