recover.go

 1package server
 2
 3import (
 4	"log/slog"
 5	"net/http"
 6	"runtime/debug"
 7)
 8
 9// recoverHandler wraps the next handler in a panic-recovery middleware.
10// If a handler panics, the panic is logged with a stack trace and a 500
11// JSON error is written to the client (when no response has been started
12// yet). Without this, a panicking handler closes the connection silently
13// and surfaces as an opaque EOF on the client side.
14func (s *Server) recoverHandler(next http.Handler) http.Handler {
15	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
16		rrw := &recoverResponseWriter{ResponseWriter: w}
17		defer func() {
18			rec := recover()
19			if rec == nil {
20				return
21			}
22			// http.ErrAbortHandler is the documented way to abort a
23			// handler without logging; preserve that contract.
24			if rec == http.ErrAbortHandler {
25				panic(rec)
26			}
27			s.logError(
28				r, "Panic in handler",
29				slog.Any("panic", rec),
30				slog.String("stack", string(debug.Stack())),
31			)
32			if !rrw.wroteHeader {
33				jsonError(rrw, http.StatusInternalServerError, "internal server error")
34			}
35		}()
36		next.ServeHTTP(rrw, r)
37	})
38}
39
40// recoverResponseWriter tracks whether the response has been started so
41// the recovery middleware knows if it can still write a 500 error.
42type recoverResponseWriter struct {
43	http.ResponseWriter
44	wroteHeader bool
45}
46
47func (rrw *recoverResponseWriter) WriteHeader(code int) {
48	rrw.wroteHeader = true
49	rrw.ResponseWriter.WriteHeader(code)
50}
51
52func (rrw *recoverResponseWriter) Write(b []byte) (int, error) {
53	rrw.wroteHeader = true
54	return rrw.ResponseWriter.Write(b)
55}
56
57func (rrw *recoverResponseWriter) Unwrap() http.ResponseWriter {
58	return rrw.ResponseWriter
59}