Learn Go← Dashboard

fmt, io, os, bufio, encoding/json, net/http, time, slog, regexp, flag, embed.

HTTP Middleware & Graceful Shutdown

The previous lesson built a tiny server. Production servers add three things on top: middleware (logging, auth, recovery), timeouts (so a slow client can't tie up a goroutine forever), and graceful shutdown (so in-flight requests finish when you deploy a new version).

Middleware as a function

A middleware is just a function func(http.Handler) http.Handler. The outer handler wraps the next one and decides whether to call it.

go playground
Loading...

The wrapping order matters: outer wrappers see requests before inner ones, and responses after. Recovery should usually be the innermost wrapper so a panic in a deeper middleware also gets caught.

Per-request values via context

Don't add fields to *http.Request; use the request context:

func withRequestID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := uuid.New().String()
        ctx := context.WithValue(r.Context(), reqIDKey{}, id)
        w.Header().Set("X-Request-ID", id)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Downstream handlers read it with r.Context().Value(reqIDKey{}). Use a private context-key type (type reqIDKey struct{}) to avoid collisions.

Server timeouts

The zero-value http.Server has no timeouts. A slow attacker (or a disconnected mobile client) can park goroutines forever. Always set them:

srv := &http.Server{
    Addr:              ":8080",
    Handler:           handler,
    ReadHeaderTimeout: 5 * time.Second,
    ReadTimeout:       15 * time.Second,
    WriteTimeout:      30 * time.Second,
    IdleTimeout:       90 * time.Second,
}
log.Fatal(srv.ListenAndServe())

ReadHeaderTimeout is the most important — it's the Slowloris defense and should be set on every public server.

Graceful shutdown

When the deploy system sends SIGTERM, you want to: stop accepting new connections, let in-flight requests finish (up to a deadline), then exit.

srv := &http.Server{Addr: ":8080", Handler: handler}

go func() {
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        log.Fatal(err)
    }
}()

stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
    log.Println("shutdown error:", err)
}

srv.Shutdown(ctx) closes the listener, then waits for active handlers to return — up to the context deadline.

Why is `ReadHeaderTimeout` more important than `ReadTimeout` on a public server?