Go HTTP Middleware Chain with Recovery
Compose request ID, structured logging, and panic recovery middleware in Go while preserving the original handler order.
Code
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httptest"
"time"
)
type Middleware func(http.Handler) http.Handler
func chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
type contextKey string
const requestIDKey contextKey = "request_id"
func requestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = fmt.Sprintf("req-%d", time.Now().UnixNano())
}
w.Header().Set("X-Request-ID", id)
ctx := context.WithValue(r.Context(), requestIDKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
type statusRecorder struct {
http.ResponseWriter
status int
bytes int
}
func (s *statusRecorder) WriteHeader(code int) {
s.status = code
s.ResponseWriter.WriteHeader(code)
}
func (s *statusRecorder) Write(body []byte) (int, error) {
if s.status == 0 {
s.status = http.StatusOK
}
n, err := s.ResponseWriter.Write(body)
s.bytes += n
return n, err
}
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
recorder := &statusRecorder{ResponseWriter: w}
next.ServeHTTP(recorder, r)
if recorder.status == 0 {
recorder.status = http.StatusOK
}
id, _ := r.Context().Value(requestIDKey).(string)
log.Printf("method=%s path=%s status=%d bytes=%d duration=%s request_id=%s",
r.Method, r.URL.Path, recorder.status, recorder.bytes,
time.Since(start).Round(time.Millisecond), id)
})
}
func recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if value := recover(); value != nil {
log.Printf("panic recovered: %v", value)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
fmt.Fprintln(w, "created")
})
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "token missing", http.StatusUnauthorized)
})
mux.HandleFunc("/boom", func(w http.ResponseWriter, r *http.Request) {
panic("unexpected nil map")
})
handler := chain(mux, requestID, logging, recovery)
for _, path := range []string{"/ok", "/hello", "/boom"} {
req := httptest.NewRequest(http.MethodGet, path, nil)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
fmt.Printf("%s -> %d request_id=%s\n", path, recorder.Code, recorder.Header().Get("X-Request-ID"))
}
}Report a problem with this exampleOpens a pre-filled issue on GitHub.