package middleware import ( "context" "log/slog" "net/http" "runtime/debug" ) // Recover returns HTTP middleware that traps panics raised by downstream // handlers. When a panic is intercepted it is logged via log with the // recovered value, request path, request ID (if present in context) and // the full stack trace, after which a 500 response is written using a // stable JSON envelope so clients see a predictable shape. // // Without this middleware a panic would unwind into net/http and abort // the connection without any structured signal, making post-mortem // debugging difficult. func Recover(log *slog.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer recoverPanic(r.Context(), log, w, r.URL.Path) next.ServeHTTP(w, r) }) } } // recoverPanic is the deferred body of Recover. Splitting it out (rather // than declaring an inline closure) lets us pass ctx explicitly so // linters can see the context propagation and keeps the middleware // itself readable. func recoverPanic(ctx context.Context, log *slog.Logger, w http.ResponseWriter, path string) { rec := recover() if rec == nil { return } log.LogAttrs(ctx, slog.LevelError, "panic recovered", slog.Any("panic", rec), slog.String("path", path), slog.String("stack", string(debug.Stack())), slog.String("request_id", RequestIDFromContext(ctx)), ) http.Error(w, `{"code":"internal","message":"internal server error"}`, http.StatusInternalServerError) }