69. Middleware
Every handler in a real service needs the same handful of things done around it: log the request, check the caller is allowed in, don't let a panic kill the process. Copying that into forty handlers is how services rot.
Middleware is the alternative, and in Go it needs no framework — just the
http.Handler interface and a closure.
A middleware is a handler that wraps a handler
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("--> %s %s\n", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
fmt.Printf("<-- %s %s done\n", r.Method, r.URL.Path)
})
}
func main() {
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "the real work")
})
h = logging(h)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/items", nil))
fmt.Println("body:", rec.Body.String())
}
--> GET /items
<-- GET /items done
body: the real work
That signature is the whole convention:
func(next http.Handler) http.Handler
Take a handler, return a new one that does something extra and calls the
original. Because the wrapper is a Handler, the thing you wrapped can't
tell, and neither can whatever you hand the result to. That's the closure work
from module 3 and the interface work from module 6 meeting at a right angle.
The three positions available to you:
- before
next.ServeHTTP— inspect or reject the request - after — observe the result
- instead — don't call
nextat all, and the inner handler never runs
Rejecting is just returning early without calling next.
Seeing the status code
The obvious logging middleware can't log what it most wants to: the status. By
the time next.ServeHTTP returns, the code has gone to the client and the
ResponseWriter won't tell you what it was. So wrap that too:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
type statusWriter struct {
http.ResponseWriter
status int
}
func (s *statusWriter) WriteHeader(code int) {
s.status = code
s.ResponseWriter.WriteHeader(code)
}
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
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")
})
mux.HandleFunc("GET /missing", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "nope", http.StatusNotFound)
})
h := logging(mux)
for _, p := range []string{"/ok", "/missing"} {
h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", p, nil))
}
}
GET /ok -> 200
GET /missing -> 404
statusWriter embeds http.ResponseWriter — the composition from module 5.
It gets every method of the interface for free, and overrides exactly one.
Anything it doesn't define falls through to the wrapped writer.
Two details that are easy to get wrong:
- The default must be
200. A handler that never callsWriteHeaderstill sends200implicitly, so a zero-valuedstatuswould log0for every successful request. WriteHeaderneeds a pointer receiver to record anything. On a value receiver you'd mutate a copy and always log the default.
Notice we wrapped the whole mux, not each route — a mux is a handler, so one
wrap covers every route registered on it.
Chaining, and which end is which
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func tag(name string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("enter", name)
next.ServeHTTP(w, r)
fmt.Println("exit ", name)
})
}
}
func chain(h http.Handler, mw ...func(http.Handler) http.Handler) http.Handler {
for i := len(mw) - 1; i >= 0; i-- {
h = mw[i](h)
}
return h
}
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println(" handler")
})
h := chain(handler, tag("outer"), tag("middle"), tag("inner"))
h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil))
}
enter outer
enter middle
enter inner
handler
exit inner
exit middle
exit outer
Middleware nests like an onion: first in the list is outermost, sees the request
first and the response last. chain builds it backwards — wrapping the last
entry first leaves the first entry on the outside, which is what makes the
argument order read the way you'd say it out loud.
Order is a correctness question, not a style one. Recovery has to be outermost or it can't catch panics from the middleware inside it. Authentication belongs before anything expensive. Logging usually goes near the outside so it measures everything.
Passing values down the chain
Middleware often learns something the handler needs — who the caller is, a request ID. It can't pass an argument, since the signature is fixed. It attaches the value to the request's context instead:
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
)
type ctxKey string
const userKey ctxKey = "user"
func authenticate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "Bearer secret" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), userKey, "ada")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func whoami(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(userKey).(string)
if !ok {
http.Error(w, "no user in context", http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "you are %s", user)
}
func main() {
h := authenticate(http.HandlerFunc(whoami))
for _, token := range []string{"Bearer secret", "Bearer wrong"} {
req := httptest.NewRequest("GET", "/me", nil)
req.Header.Set("Authorization", token)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
fmt.Printf("%-14s %d %q\n", token, rec.Code, rec.Body.String())
}
}
Bearer secret 200 "you are ada"
Bearer wrong 401 "unauthorized\n"
The rejection path returns without calling next, so whoami never runs.
ctxKey is a defined type, not a plain string, and that matters. Context
keys are compared by type as well as value, so an unexported ctxKey("user")
cannot collide with a "user" key set by a library you imported. Using a bare
string here is a real bug waiting to happen, and go vet will tell you so.
Read the value back with the comma-ok assertion from module 6. Value returns
any, and it returns nil when the key is absent — so a bare .(string) would
panic on any request that skipped the middleware.
Keep context values for request-scoped data that middleware produced. It is not a general-purpose bag for passing arguments: the compiler can't check what's in there, so anything you can pass as a normal function parameter should be.
Recovering from panics
A panic in a handler doesn't just fail one request. Left alone it unwinds through the server goroutine, and module 7's rule applies — an unrecovered panic takes the whole process with it. One bad request would end the service for everyone.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
fmt.Println("recovered from:", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func main() {
boom := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
panic("database connection lost")
})
h := recovery(boom)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/boom", nil))
fmt.Printf("status %d body %q\n", rec.Code, rec.Body.String())
ok := recovery(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "no panic here")
}))
rec2 := httptest.NewRecorder()
ok.ServeHTTP(rec2, httptest.NewRequest("GET", "/fine", nil))
fmt.Printf("status %d body %q\n", rec2.Code, rec2.Body.String())
}
24:3: panic: main.main.func(...)
recovered from: database connection lost
status 500 body "internal server error\n"
status 200 body "no panic here"
That first line is the in-browser interpreter announcing the panic it saw;
compiled Go doesn't print it, and you can ignore it. Everything below it is what
real Go produces: the recover() returned exactly the value passed to panic,
the client got a clean 500 instead of a dropped connection, and the next
request was unaffected.
defer runs on the way out of the wrapper whether or not anything panicked,
which is the only reason this works — the deferred closure is already scheduled
before next.ServeHTTP is ever called.
Two rules for real recovery middleware. Log the panic, with a stack trace
(runtime/debug.Stack()) — a swallowed panic is worse than a crash, because
the bug is now invisible. And never put the panic value in the response; it
routinely contains internal detail. The client gets a generic 500, your logs
get everything.
Note that net/http's own server already recovers panics per connection, so a
panicking handler will not usually kill the process. Write this middleware
anyway: the built-in behaviour drops the connection with no response at all,
which the client sees as a broken pipe rather than a 500.
Your turn
Write a requireAPIKey middleware that lets a request through only if the
X-API-Key header is exactly letmein, and otherwise responds 403 with the
body forbidden — without running the wrapped handler.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
// requireAPIKey should let the request through only when
// the X-API-Key header is "letmein", else 403 "forbidden".
func requireAPIKey(next http.Handler) http.Handler {
return nil
}
func main() {
secret := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "the secret")
})
h := requireAPIKey(secret)
for _, key := range []string{"letmein", "guess", ""} {
req := httptest.NewRequest("GET", "/secret", nil)
if key != "" {
req.Header.Set("X-API-Key", key)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
fmt.Printf("key=%-9q %d %q\n", key, rec.Code, rec.Body.String())
}
}
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func requireAPIKey(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-API-Key") != "letmein" {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
secret := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "the secret")
})
h := requireAPIKey(secret)
for _, key := range []string{"letmein", "guess", ""} {
req := httptest.NewRequest("GET", "/secret", nil)
if key != "" {
req.Header.Set("X-API-Key", key)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
fmt.Printf("key=%-9q %d %q\n", key, rec.Code, rec.Body.String())
}
}
Next: those fmt.Printf log lines are not what you want in production.