HTTP middleware that recovers panics cleanly
A panic in an HTTP handler crashes the goroutine handling the request, which in net/http does not crash the whole server but does log a default message that leaks internal details into access logs. I always wrap handlers with a recover middleware that produces a clean 500 and logs a structured stack trace.
func Recover(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
v := recover()
if v == nil {
return
}
// Do not handle http.ErrAbortHandler: it is the intended way to abort.
if err, ok := v.(error); ok && errors.Is(err, http.ErrAbortHandler) {
panic(v)
}
buf := make([]byte, 16<<10)
n := runtime.Stack(buf, false)
slog.ErrorContext(r.Context(),
"panic in handler",
"recovered", fmt.Sprint(v),
"stack", string(buf[:n]),
"method", r.Method,
"path", r.URL.Path,
)
http.Error(w, "internal server error", http.StatusInternalServerError)
}()
next.ServeHTTP(w, r)
})
}
Two things people miss:
- Re-panic on
http.ErrAbortHandler. The server uses it as a signal to tear down the connection without writing a response; swallowing it breaks that contract. - Pass
runtime.Stack(..., false)for the current goroutine only.truedumps every goroutine in the process, which is rarely what you want in a per-request log.
See also /posts/the-panic-in-a-goroutine-that-took-down-prod/.