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}