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
This commit is contained in:
173
pkg/errors/handler.go
Normal file
173
pkg/errors/handler.go
Normal file
@@ -0,0 +1,173 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user