xlgo-core
88 行
3.1 KiB
Go
88 行
3.1 KiB
Go
package middleware
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"runtime/debug"
|
||
|
||
"github.com/EthanCodeCraft/xlgo-core/logger"
|
||
"github.com/EthanCodeCraft/xlgo-core/response"
|
||
"github.com/gin-gonic/gin"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// Recover panic恢复中间件,捕获panic并返回统一错误响应
|
||
func Recover() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
defer func() {
|
||
if err := recover(); err != nil {
|
||
// 获取请求ID
|
||
requestID := GetRequestID(c)
|
||
|
||
// 记录错误日志
|
||
logger.Error("panic 已恢复",
|
||
zap.String("request_id", requestID),
|
||
zap.String("error", fmt.Sprintf("%v", err)),
|
||
zap.String("path", c.Request.URL.Path),
|
||
zap.String("method", c.Request.Method),
|
||
zap.String("stack", string(debug.Stack())),
|
||
)
|
||
|
||
// 返回错误响应。
|
||
// 必须用 response.Custom 显式写 HTTP 500:FailWithCode 经 writeResp→httpStatusFor,
|
||
// 在默认 ModeBusiness 下对 CodeServerError 返回 200,c.JSON(200,...) 会先 flush
|
||
// 锁定状态,随后 AbortWithStatus(500) 因 w.Written()==true 沦为 no-op,
|
||
// 导致客户端收到 HTTP 200 + body code:500,网关/APM 按 status 看不到 panic。
|
||
// Custom 不受 Mode 影响,直接写 500 且保留 RequestID。
|
||
//
|
||
// M-B 修复:若 panic 发生在响应已开始写出之后(流式/部分写),c.Writer.Written()==true,
|
||
// gin 拒绝再次写响应,response.Custom 沦为 no-op,客户端会收到截断响应而非 500。
|
||
// 此时不再尝试写 500(无效),仅 Abort + 记录,避免无意义的二次写入。
|
||
if !c.Writer.Written() {
|
||
response.Custom(c, http.StatusInternalServerError, response.CodeServerError, "服务器内部错误", nil)
|
||
} else {
|
||
logger.Warn("panic 发生在响应已开始写入后,无法写入 500,客户端将收到截断响应",
|
||
zap.String("request_id", requestID),
|
||
zap.String("path", c.Request.URL.Path),
|
||
zap.Int("bytes_written", c.Writer.Size()),
|
||
)
|
||
}
|
||
c.Abort()
|
||
}
|
||
}()
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// RecoverWithDetail panic恢复中间件(详细版本,返回更多信息)
|
||
// 注意: 生产环境不应使用,会暴露敏感信息
|
||
func RecoverWithDetail() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
defer func() {
|
||
if err := recover(); err != nil {
|
||
requestID := GetRequestID(c)
|
||
|
||
logger.Error("panic 已恢复",
|
||
zap.String("request_id", requestID),
|
||
zap.String("error", fmt.Sprintf("%v", err)),
|
||
zap.String("path", c.Request.URL.Path),
|
||
zap.String("method", c.Request.Method),
|
||
zap.String("stack", string(debug.Stack())),
|
||
)
|
||
|
||
// 开发环境返回详细错误。同样用 Custom 显式写 500(见 Recover 注释)。
|
||
// M-B 修复:响应已写出时不重复写 500(同 Recover)。
|
||
if !c.Writer.Written() {
|
||
response.Custom(c, http.StatusInternalServerError, response.CodeServerError, fmt.Sprintf("服务器内部错误: %v", err), nil)
|
||
} else {
|
||
logger.Warn("panic 发生在响应已开始写入后,无法写入 500,客户端将收到截断响应",
|
||
zap.String("request_id", requestID),
|
||
zap.String("path", c.Request.URL.Path),
|
||
)
|
||
}
|
||
c.Abort()
|
||
}
|
||
}()
|
||
c.Next()
|
||
}
|
||
} |