package http
import (
"fmt"
"net/http"
"runtime/debug"
)
// RecoveryOptions tunes the recovery middleware.
type RecoveryOptions struct {
// Status is the HTTP response code written on panic. Defaults to 500.
Status int
// Body is the response body written on panic. Defaults to "internal
// server error".
Body string
// IncludeStack controls whether the stack trace is emitted as a log
// field. Leave false in production to avoid leaking internals into
// logs that may be shipped to shared destinations.
IncludeStack bool
}
// Recovery returns a middleware that catches panics, logs them, and writes
// a well-formed response to the client instead of letting the Lambda
// runtime return a bare 502.
func Recovery(fallback Logger, opts RecoveryOptions) func(http.Handler) http.Handler {
status := opts.Status
if status == 0 {
status = http.StatusInternalServerError
}
body := opts.Body
if body == "" {
body = "internal server error"
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
rec := recover()
if rec == nil {
return
}
l := LoggerFrom(r.Context(), fallback)
fs := []Field{{Key: "panic", Value: fmt.Sprint(rec)}}
if opts.IncludeStack {
fs = append(fs, Field{Key: "stack", Value: string(debug.Stack())})
}
l.Error("http.panic", fs...)
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(status)
_, _ = w.Write([]byte(body))
}()
next.ServeHTTP(w, r)
})
}
}
// PanicAsError is a helper used by tests that want to force a panic out of
// a handler without calling into arbitrary user code.
func PanicAsError(v any) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
panic(v)
})
}
// RecoveredError wraps a recovered panic value and the captured stack. It
// is exported so tests can assert on the error chain.
type RecoveredError struct {
Value any
Stack []byte
}
// Error implements error.
func (e *RecoveredError) Error() string {
return fmt.Sprintf("recovered panic: %v", e.Value)
}