176 lines
4.3 KiB
Go
176 lines
4.3 KiB
Go
package errors
|
||
|
||
import (
|
||
"runtime/debug"
|
||
"time"
|
||
|
||
"github.com/gofiber/fiber/v2"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// SafeErrorHandler 返回受保护的 Fiber ErrorHandler
|
||
// 使用 defer/recover 防止 ErrorHandler 自身 panic 导致服务崩溃
|
||
func SafeErrorHandler(logger *zap.Logger) fiber.ErrorHandler {
|
||
return func(c *fiber.Ctx, err error) error {
|
||
// 使用 defer/recover 保护 ErrorHandler 自身
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
// ErrorHandler 自身发生 panic,记录日志并返回空响应
|
||
logger.Error("ErrorHandler panic",
|
||
zap.Any("panic", r),
|
||
zap.String("stack", string(debug.Stack())),
|
||
)
|
||
// 返回 500 空响应体,避免泄露错误信息
|
||
_ = c.Status(500).SendString("")
|
||
}
|
||
}()
|
||
|
||
// 调用核心错误处理逻辑
|
||
return handleError(c, err, logger)
|
||
}
|
||
}
|
||
|
||
// handleError 核心错误处理逻辑
|
||
func handleError(c *fiber.Ctx, err error, logger *zap.Logger) error {
|
||
// 1. 检查响应是否已发送
|
||
if c.Response().StatusCode() != fiber.StatusOK || len(c.Response().Body()) > 0 {
|
||
// 响应已发送,仅记录日志,不修改响应
|
||
safeLog(logger, "响应已发送后发生错误",
|
||
zap.Error(err),
|
||
zap.Int("status", c.Response().StatusCode()),
|
||
zap.Int("body_size", len(c.Response().Body())),
|
||
)
|
||
return nil
|
||
}
|
||
|
||
// 2. 提取错误上下文
|
||
errCtx := FromFiberContext(c)
|
||
|
||
// 3. 错误类型分类和处理
|
||
var code int
|
||
var message string
|
||
var httpStatus int
|
||
|
||
switch e := err.(type) {
|
||
case *AppError:
|
||
// 应用自定义错误
|
||
code = e.Code
|
||
message = e.Message
|
||
httpStatus = GetHTTPStatus(e.Code)
|
||
|
||
// 记录错误日志(包含完整上下文)
|
||
logFields := append(errCtx.ToLogFields(),
|
||
zap.Int("error_code", code),
|
||
zap.Error(err),
|
||
)
|
||
|
||
// 根据错误类型决定日志级别
|
||
logLevel := GetLogLevel(code)
|
||
if logLevel == "error" {
|
||
// 服务端错误 -> Error 级别
|
||
safeLogWithLevel(logger, "error", "服务端错误", logFields...)
|
||
} else {
|
||
// 客户端错误 -> Warn 级别
|
||
safeLogWithLevel(logger, "warn", "客户端错误", logFields...)
|
||
}
|
||
|
||
case *fiber.Error:
|
||
// Fiber 框架错误
|
||
httpStatus = e.Code
|
||
code = mapHTTPStatusToCode(httpStatus)
|
||
message = GetMessage(code, "zh")
|
||
|
||
safeLog(logger, "Fiber 框架错误",
|
||
append(errCtx.ToLogFields(),
|
||
zap.Int("http_status", httpStatus),
|
||
zap.String("fiber_message", e.Message),
|
||
)...,
|
||
)
|
||
|
||
default:
|
||
// 其他未知错误,默认为内部服务器错误
|
||
code = CodeInternalError
|
||
httpStatus = 500
|
||
message = GetMessage(CodeInternalError, "zh")
|
||
|
||
safeLog(logger, "未知错误",
|
||
append(errCtx.ToLogFields(),
|
||
zap.Error(err),
|
||
)...,
|
||
)
|
||
}
|
||
|
||
// 4. 敏感信息脱敏:所有 5xx 错误返回通用消息
|
||
if httpStatus >= 500 {
|
||
message = GetMessage(code, "zh")
|
||
}
|
||
|
||
// 5. 设置响应 Header X-Request-ID
|
||
if errCtx.RequestID != "" {
|
||
c.Set("X-Request-ID", errCtx.RequestID)
|
||
}
|
||
|
||
// 6. 返回统一 JSON 响应
|
||
errResp := c.Status(httpStatus).JSON(fiber.Map{
|
||
"code": code,
|
||
"data": nil,
|
||
"msg": message,
|
||
"timestamp": time.Now().Format(time.RFC3339),
|
||
})
|
||
c.Set(fiber.HeaderContentType, "application/json; charset=utf-8")
|
||
return errResp
|
||
}
|
||
|
||
// safeLog 安全地记录日志,日志失败时静默处理(默认 Error 级别)
|
||
func safeLog(logger *zap.Logger, msg string, fields ...zap.Field) {
|
||
safeLogWithLevel(logger, "error", msg, fields...)
|
||
}
|
||
|
||
// safeLogWithLevel 安全地记录指定级别的日志,日志失败时静默处理
|
||
func safeLogWithLevel(logger *zap.Logger, level string, msg string, fields ...zap.Field) {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
// 日志系统 panic,静默丢弃,不阻塞响应
|
||
// 不记录到任何地方,避免无限循环
|
||
}
|
||
}()
|
||
|
||
switch level {
|
||
case "warn":
|
||
logger.Warn(msg, fields...)
|
||
case "error":
|
||
logger.Error(msg, fields...)
|
||
case "info":
|
||
logger.Info(msg, fields...)
|
||
default:
|
||
logger.Error(msg, fields...)
|
||
}
|
||
}
|
||
|
||
// mapHTTPStatusToCode 将 HTTP 状态码映射为应用错误码
|
||
func mapHTTPStatusToCode(status int) int {
|
||
switch status {
|
||
case 400:
|
||
return CodeInvalidParam
|
||
case 401:
|
||
return CodeUnauthorized
|
||
case 403:
|
||
return CodeForbidden
|
||
case 404:
|
||
return CodeNotFound
|
||
case 409:
|
||
return CodeConflict
|
||
case 429:
|
||
return CodeTooManyRequests
|
||
case 503:
|
||
return CodeServiceUnavailable
|
||
case 504:
|
||
return CodeTimeout
|
||
default:
|
||
if status >= 500 {
|
||
return CodeInternalError
|
||
}
|
||
return CodeInvalidParam
|
||
}
|
||
}
|