-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrecovery.go
More file actions
191 lines (168 loc) · 5.18 KB
/
recovery.go
File metadata and controls
191 lines (168 loc) · 5.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// Copyright 2022 Sylvain Müller. All rights reserved.
// Mount of this source code is governed by a Apache-2.0 license that can be found
// at https://github.com/fox-toolkit/fox/blob/master/LICENSE.txt.
package fox
import (
"bytes"
"errors"
"fmt"
"iter"
"log/slog"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime"
"slices"
"strings"
"github.com/fox-toolkit/fox/internal/iterutil"
)
// Keys for "built-in" logger attributes used by the recovery middleware.
const (
// LoggerRouteKey is the key used by the built-in recovery middleware for the matched route
// when the log method is called. The associated [slog.Value] is a string.
LoggerRouteKey = "route"
// LoggerParamsKey is the key used by the built-in recovery middleware for route parameters
// when the log method is called. The associated [slog.Value] is a [slog.GroupValue] containing parameter
// key-value pairs.
LoggerParamsKey = "params"
// LoggerPanicKey is the key used by the built-in recovery middleware for the panic value
// when the log method is called. The associated [slog.Value] is any.
LoggerPanicKey = "panic"
)
var reqHeaderSep = []byte("\r\n")
// RecoveryFunc is a function type that defines how to handle panics that occur during the
// handling of an HTTP request.
type RecoveryFunc func(c *Context, err any)
// RecoveryWithFunc returns a middleware that recovers from any panics, logs the error, request details, and stack trace
// using the provided [slog.Handler] and then calls the handle function to handle the recovery.
func RecoveryWithFunc(handler slog.Handler, handle RecoveryFunc) MiddlewareFunc {
slogger := slog.New(handler)
return func(next HandlerFunc) HandlerFunc {
return func(c *Context) {
defer recovery(slogger, c, handle)
next(c)
}
}
}
// Recovery returns a middleware that recovers from any panics, logs the error, request details, and stack trace
// using the provided [slog.Handler] and writes a 500 status code response if a panic occurs.
func Recovery(handler slog.Handler) MiddlewareFunc {
return RecoveryWithFunc(handler, DefaultHandleRecovery)
}
// DefaultHandleRecovery is a default implementation of the [RecoveryFunc].
// It responds with a status code 500 and writes a generic error message.
func DefaultHandleRecovery(c *Context, _ any) {
http.Error(c.Writer(), http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
func recovery(logger *slog.Logger, c *Context, handle RecoveryFunc) {
if err := recover(); err != nil {
if e, ok := err.(error); ok && errors.Is(e, http.ErrAbortHandler) {
panic(e)
}
var sb strings.Builder
sb.WriteString("Recovered from PANIC\n")
httpRequest, _ := httputil.DumpRequest(c.Request(), false)
sb.Grow(len(httpRequest))
if before, after, found := bytes.Cut(httpRequest, reqHeaderSep); found {
sb.WriteString("Request Dump:\n")
sb.Write(before)
for header := range iterutil.SplitBytesSeq(after, reqHeaderSep) {
sb.Write(reqHeaderSep)
before0, _, ok := bytes.Cut(header, []byte{':'})
if !ok {
continue
}
if slices.Contains(blacklistedHeader, string(before0)) {
sb.Write(before0)
sb.WriteString(": <redacted>")
continue
}
sb.Write(header)
}
}
sb.WriteString("Stack:\n")
sb.WriteString(stacktrace(3, 6))
var params []any
if c.Route() != nil {
params = make([]any, 0, c.Route().ParamsLen())
params = slices.AppendSeq(params, mapParamsToAttr(c.Params()))
}
pattern := c.Pattern()
if pattern == "" {
pattern = scopeToString(c.Scope())
}
logger.Error(
sb.String(),
slog.String(LoggerRouteKey, pattern),
slog.Group(LoggerParamsKey, params...),
slog.Any(LoggerPanicKey, err),
)
if !c.Writer().Written() && !connIsBroken(err) {
handle(c, err)
}
}
}
func connIsBroken(err any) bool {
//goland:noinspection GoTypeAssertionOnErrors
if ne, ok := err.(*net.OpError); ok {
if se, ok2 := errors.AsType[*os.SyscallError](ne); ok2 {
seStr := strings.ToLower(se.Error())
return strings.Contains(seStr, "broken pipe") || strings.Contains(seStr, "connection reset by peer")
}
}
return false
}
func stacktrace(skip, nFrames int) string {
pcs := make([]uintptr, nFrames+1)
n := runtime.Callers(skip+1, pcs)
if n == 0 {
return "(no stack)"
}
frames := runtime.CallersFrames(pcs[:n])
var b strings.Builder
i := 0
for {
frame, more := frames.Next()
if i > 0 {
b.WriteByte('\n')
}
_, _ = fmt.Fprintf(&b, "called from %s %s:%d", frame.Function, frame.File, frame.Line)
if !more {
break
}
i++
if i >= nFrames {
_, _ = fmt.Fprintf(&b, "\n(rest of stack elided)")
break
}
}
return b.String()
}
func mapParamsToAttr(params iter.Seq[Param]) iter.Seq[any] {
return func(yield func(any) bool) {
for p := range params {
if !yield(slog.String(p.Key, p.Value)) {
break
}
}
}
}
func scopeToString(scope HandlerScope) string {
var strScope string
switch scope {
case OptionsHandler:
strScope = "OptionsHandler"
case NoMethodHandler:
strScope = "NoMethodHandler"
case RedirectSlashHandler:
strScope = "RedirectSlashHandler"
case RedirectPathHandler:
strScope = "RedirectPathHandler"
case NoRouteHandler:
strScope = "NoRouteHandler"
default:
strScope = "UnknownHandler"
}
return strScope
}