middleware/http/recovery.go

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)
}