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 = e.HTTPStatus // 记录错误日志(包含完整上下文) 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 响应 return c.Status(httpStatus).JSON(fiber.Map{ "code": code, "data": nil, "msg": message, "timestamp": time.Now().Format(time.RFC3339), }) } // 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 } }