Files
junhong_cmp_fiber/pkg/errors/handler.go
huang fb83c9a706 feat: 实现统一错误处理系统 (003-error-handling)
- 新增统一错误码定义和管理 (pkg/errors/codes.go)
- 新增全局错误处理器和中间件 (pkg/errors/handler.go, internal/middleware/error_handler.go)
- 新增错误上下文管理 (pkg/errors/context.go)
- 增强 Panic 恢复中间件 (internal/middleware/recover.go)
- 新增完整的单元测试和集成测试
- 新增功能文档 (docs/003-error-handling/)
- 新增功能规范 (specs/003-error-handling/)
- 更新 CLAUDE.md 和 README.md
2025-11-15 12:17:44 +08:00

174 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}