Go · 1779 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package middleware
4
5 import (
6 "fmt"
7 "log/slog"
8 "net/http"
9 "runtime/debug"
10
11 "github.com/tenseleyFlow/shithub/internal/infra/errrep"
12 "github.com/tenseleyFlow/shithub/internal/infra/metrics"
13 )
14
15 // PanicHandler renders a styled error response when the request handler
16 // panics. The error page receives the request_id so users can quote it for
17 // support.
18 type PanicHandler interface {
19 HandlePanic(w http.ResponseWriter, r *http.Request, requestID string, recovered any)
20 }
21
22 // Recover returns middleware that catches panics, logs them with the
23 // request_id, and delegates rendering to handler. If handler is nil a plain
24 // "internal server error" body is written.
25 func Recover(logger *slog.Logger, handler PanicHandler) func(http.Handler) http.Handler {
26 return func(next http.Handler) http.Handler {
27 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28 defer func() {
29 if rec := recover(); rec != nil {
30 if rec == http.ErrAbortHandler {
31 panic(rec)
32 }
33 metrics.PanicsTotal.Inc()
34 reqID := RequestIDFromContext(r.Context())
35 if logger != nil {
36 logger.ErrorContext(
37 r.Context(),
38 "panic in handler",
39 slog.String("request_id", reqID),
40 slog.Any("panic", rec),
41 slog.String("stack", string(debug.Stack())),
42 )
43 }
44 errrep.CapturePanic(rec, reqID)
45 if handler != nil {
46 handler.HandlePanic(w, r, reqID, rec)
47 return
48 }
49 if w.Header().Get("Content-Type") == "" {
50 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
51 }
52 w.WriteHeader(http.StatusInternalServerError)
53 _, _ = fmt.Fprintf(w, "internal server error (request_id=%s)\n", reqID)
54 }
55 }()
56 next.ServeHTTP(w, r)
57 })
58 }
59 }
60