package middleware import ( "errors" "log/slog" "net" "net/http" "net/http/httputil" "net/url" "os" "runtime/debug" "strings" "github.com/gin-gonic/gin" ) func Recovery(logger *slog.Logger) gin.HandlerFunc { return func(c *gin.Context) { defer func() { panicValue := recover() if panicValue == nil { return } brokenPipe := isBrokenPipe(panicValue) request := recoveryRequestDump(c.Request) if logger != nil { logger.ErrorContext(c.Request.Context(), "recovery from panic", "mod", "http", "error", panicValue, "request", request, "stack", string(debug.Stack())) } if brokenPipe { if err, ok := panicValue.(error); ok { _ = c.Error(err) } c.Abort() return } c.AbortWithStatus(http.StatusInternalServerError) }() c.Next() } } func isBrokenPipe(value any) bool { var networkError *net.OpError if !errors.As(asError(value), &networkError) { return false } var syscallError *os.SyscallError if !errors.As(networkError, &syscallError) { return false } message := strings.ToLower(syscallError.Error()) return strings.Contains(message, "broken pipe") || strings.Contains(message, "connection reset by peer") } func asError(value any) error { err, _ := value.(error) return err } func recoveryRequestDump(request *http.Request) string { if request == nil { return "" } clone := request.Clone(request.Context()) clone.Header = request.Header.Clone() for key := range clone.Header { switch strings.ToLower(key) { case "authorization", "cookie", "set-cookie", "x-token", "proxy-authorization": clone.Header[key] = []string{"***"} } } if request.URL != nil { clone.URL = new(url.URL) *clone.URL = *request.URL clone.URL.RawQuery = redactQuery(request.URL.RawQuery) clone.RequestURI = clone.URL.RequestURI() } dumped, err := httputil.DumpRequest(clone, false) if err != nil { return clone.Method + " " + clone.RequestURI } return string(dumped) }