package middleware import ( "encoding/json" "log/slog" "net/http" "runtime/debug" ) // Recoverer turns panics into 500 JSON responses without leaking the stack to // clients. The full stack is logged at error level. func Recoverer(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rv := recover(); rv != nil { slog.Error("panic recovered", "path", r.URL.Path, "method", r.Method, "err", rv, "stack", string(debug.Stack()), ) if !headerWritten(w) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) _ = json.NewEncoder(w).Encode(map[string]string{ "error": "InternalError", "detail": "internal server error", }) } } }() next.ServeHTTP(w, r) }) } // headerWritten is best-effort; if the response is hijacked we skip writing. func headerWritten(w http.ResponseWriter) bool { if rw, ok := w.(interface{ Status() int }); ok { return rw.Status() != 0 } return false }