Files

105 lines
2.7 KiB
Go

package middleware
import (
"errors"
"fmt"
"net/http"
"runtime/debug"
"syscall"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/dujiao-next/internal/i18n"
"github.com/dujiao-next/internal/logger"
"github.com/dujiao-next/internal/platform/http/response"
)
// RecoveryMiddleware 捕获 panic,结构化记录日志并返回 i18n 的 500 错误体,
// 确保前端收到统一的响应格式;同时把 request_id / method / path / stack 等
// 字段以 JSON 形式写入 zap 日志,运维可通过 grep / Loki / ELK 等做告警。
//
// 注意:对于客户端断开类 panic(EPIPE / ECONNRESET / http.ErrAbortHandler),
// 仅记录 warn 日志,不尝试写响应体——这些 panic 是网络层噪音,不是应用 bug,
// 向死 socket 写入注定失败。与 gin.Recovery 的默认行为保持一致。
func RecoveryMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
r := recover()
if r == nil {
return
}
requestID := getRequestID(c)
path := c.Request.URL.Path
method := c.Request.Method
clientIP := c.ClientIP()
// 客户端断开类 panic 单独处理:warn 日志 + Abort,不写响应体。
if isBrokenPipe(r) {
logger.Z().Warn("client_disconnected_panic",
zap.String("request_id", requestID),
zap.String("method", method),
zap.String("path", path),
zap.String("client_ip", clientIP),
zap.String("reason", fmt.Sprintf("%v", r)),
)
if e, ok := r.(error); ok {
_ = c.Error(e)
}
c.Abort()
return
}
stack := debug.Stack()
err := normalizePanic(r)
logger.Z().Error("panic_recovered",
zap.String("request_id", requestID),
zap.String("method", method),
zap.String("path", path),
zap.String("client_ip", clientIP),
zap.Error(err),
zap.ByteString("stack", stack),
)
if !c.Writer.Written() {
msg := i18n.T(i18n.ResolveLocale(c), "error.internal_server_error")
if msg == "" {
msg = "Internal Server Error"
}
response.ErrorWithHTTPStatus(c, http.StatusInternalServerError, http.StatusInternalServerError, msg)
}
c.Abort()
}()
c.Next()
}
}
// isBrokenPipe 判断 panic 值是否为客户端断开类网络错误。
func isBrokenPipe(r interface{}) bool {
e, ok := r.(error)
if !ok {
return false
}
if errors.Is(e, http.ErrAbortHandler) {
return true
}
// errors.Is 会沿着 wrap chain / *net.OpError 链查找 syscall 错误
if errors.Is(e, syscall.EPIPE) || errors.Is(e, syscall.ECONNRESET) {
return true
}
return false
}
func normalizePanic(r interface{}) error {
switch v := r.(type) {
case error:
return v
case string:
return fmt.Errorf("panic: %s", v)
default:
return fmt.Errorf("panic: %v (type=%T)", v, v)
}
}