HTTP Middleware
Middleware is a function that takes an http.Handler and returns a new one wrapping it, letting you run logging, authentication, panic recovery, and other cross-cutting logic around every request without touching your handlers.
Learn HTTP Middleware in our free Go course — an interactive lesson with runnable examples, a practice exercise and a quick reference.
Part of the free Go course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ The middleware shape
Middleware has one shape: func(next http.Handler) http.Handler . It returns a new handler (usually an http.HandlerFunc ) that does something, then calls next.ServeHTTP(w, r) to invoke the wrapped handler — and may do more afterward. Because input and output are both http.Handler , middleware composes freely.
2️⃣ Chaining and short-circuiting
Stack middleware by wrapping: logging(auth(handler)) runs logging outermost. A small chain helper lets you list them outermost-first. A middleware short-circuits by writing a response and returning without calling next — that's how auth blocks a request with no token.
3️⃣ Wrapping ResponseWriter & recovering from panics
To log the status code , you must capture it — the plain ResponseWriter won't tell you. Embed it in a struct and override WriteHeader to remember the code (defaulting to 200). And recovery middleware uses defer + recover() to turn a handler panic into a clean 500 instead of an empty response.
🎯 Your Turn
Write a tiny middleware that adds a response header. Fill in the two blanks marked ___ , then run it.
❌ Forgetting next.ServeHTTP(w, r) — the chain dead-ends; the handler never runs.
✅ Call next exactly once in any pass-through middleware.
❌ Wrong wrap order — e.g. logging the status before recovery can catch a panic.
✅ Recovery outermost, then logging, then auth, then the handler.
❌ Logging the status without wrapping ResponseWriter — you can't read it back.
✅ Embed http.ResponseWriter and override WriteHeader (default to 200).
❌ Continuing after a short-circuit — writing 401 then still calling next sends two responses.
✅ return immediately after writing the early response.
A (outermost) sees the request first, then B, then the handler — and the response unwinds back out through B then A.
200 — Go sends 200 OK by default on the first write, so your wrapper must initialise its recorded status to 200.
Write middleware that increments a counter per request and sets an X-Request-Count header to the new value. Wrap a handler with it, fire 3 requests, and print each count.
Practice quiz
What is the common signature of HTTP middleware in Go?
- func(http.Request) error
- func(http.Handler) http.Handler
- func() http.Handler
- func(http.ResponseWriter)
Answer: func(http.Handler) http.Handler. Middleware takes the next handler and returns a new handler: func(http.Handler) http.Handler.
How does middleware pass control to the wrapped handler?
- By returning
- By calling next.ServeHTTP(w, r)
- By panicking
- Automatically
Answer: By calling next.ServeHTTP(w, r). Calling next.ServeHTTP(w, r) invokes the next handler in the chain.
How can middleware short-circuit a request (e.g. reject it)?
- Call next twice
- Return early without calling next.ServeHTTP
- Throw an error
- Close the server
Answer: Return early without calling next.ServeHTTP. Writing a response and returning before calling next.ServeHTTP stops the chain — useful for auth checks.
When you apply several middlewares, which one is the outermost layer?
- The last applied
- The first applied
- The shortest one
- Order does not matter
Answer: The first applied. The first middleware applied wraps the rest, making it the outermost layer.
How do you capture the HTTP status code a handler writes?
- Read it from the request
- Wrap ResponseWriter and override WriteHeader
- It is impossible
- Use a global variable
Answer: Wrap ResponseWriter and override WriteHeader. Wrap http.ResponseWriter and override WriteHeader to record the status before delegating.
What turns a panic in a handler into a clean 500 response?
- os.Exit
- recover() inside deferred middleware
- panic() again
- log.Fatal
Answer: recover() inside deferred middleware. A deferred recover() in recovery middleware catches the panic and writes a 500 instead of crashing.
Why is http.Handler an interface a good fit for middleware?
- It is faster
- Any wrapper that satisfies it can be composed transparently
- It avoids imports
- It is required by the compiler
Answer: Any wrapper that satisfies it can be composed transparently. Because handlers are an interface, wrappers compose freely without callers knowing the difference.
Can middleware run logic both before and after the next handler?
- No, only before
- No, only after
- Yes — code before and after next.ServeHTTP
- Only with goroutines
Answer: Yes — code before and after next.ServeHTTP. Code before next.ServeHTTP runs on the way in; code after runs on the way out (e.g. timing).
Since http.ServeMux is itself an http.Handler, you can:
- Only wrap individual handlers
- Wrap the whole mux to apply middleware to every route
- Not use middleware with it
- Only wrap GET routes
Answer: Wrap the whole mux to apply middleware to every route. ServeMux satisfies http.Handler, so wrapping the mux applies middleware to all routes.
What is a typical cross-cutting concern handled by middleware?
- Business logic
- Logging, recovery, and CORS
- Database schema
- Struct definitions
Answer: Logging, recovery, and CORS. Logging, panic recovery, CORS, and auth are classic cross-cutting concerns done in middleware.