- 新增统一错误码定义和管理 (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
12 KiB
12 KiB
Research: Fiber 错误处理集成
Feature: 003-error-handling
Date: 2025-11-14
Status: Complete
研究目标
解决实施 Fiber 错误处理集成时的技术不确定性和最佳实践。
研究任务
1. Fiber 框架错误处理机制
研究问题: Fiber 如何实现全局错误处理?如何与中间件链配合?
决策: 使用 Fiber 的 ErrorHandler 配置项实现全局错误处理
技术方案:
app := fiber.New(fiber.Config{
ErrorHandler: customErrorHandler,
// ... 其他配置
})
func customErrorHandler(c *fiber.Ctx, err error) error {
// 1. 检查是否已发送响应
if c.Response().StatusCode() != fiber.StatusOK {
// 已发送响应,仅记录日志
logger.Error("响应已发送后发生错误", zap.Error(err))
return nil
}
// 2. 处理不同类型的错误
// 3. 返回统一格式的错误响应
}
理由:
- Fiber 的
ErrorHandler是捕获所有返回错误的最后一道防线 - 与中间件链自然集成,所有
c.Next()返回的错误都会被捕获 - 可以统一处理来自不同层(Handler、Middleware)的错误
参考资料:
- Fiber Error Handling
- 现有代码:
cmd/api/main.go中的 Fiber 应用配置
2. ErrorHandler 自身保护机制
研究问题: 如何防止 ErrorHandler 本身发生错误或 panic 导致无限循环或服务崩溃?
决策: 使用 defer + recover 保护 ErrorHandler,失败时返回最简响应
技术方案:
func SafeErrorHandler(logger *zap.Logger) fiber.ErrorHandler {
return func(c *fiber.Ctx, err error) error {
defer func() {
if r := recover(); r != nil {
// ErrorHandler 自身 panic,返回空响应避免崩溃
logger.Error("ErrorHandler panic",
zap.Any("panic", r),
zap.String("stack", string(debug.Stack())),
)
_ = c.Status(500).SendString("") // 空响应体
}
}()
// 正常的错误处理逻辑
return handleError(c, err, logger)
}
}
理由:
- 符合 spec.md FR-009 要求:ErrorHandler 必须使用 defer + recover 保护
- 当 ErrorHandler 失败时返回 HTTP 500 空响应体,避免泄露错误信息
- 确保即使 ErrorHandler 崩溃也不会影响服务可用性
- 将 panic 详情记录到日志以供排查
边界情况:
- 日志系统不可用:静默失败,丢弃日志(符合 spec.md clarification)
- JSON 序列化失败:已被 defer/recover 捕获,返回空响应
3. 敏感信息识别和隐藏
研究问题: 如何自动识别并隐藏错误消息中的敏感信息(数据库错误、文件路径、密钥等)?
决策: 为所有内部错误返回通用错误消息,原始错误仅记录到日志
技术方案:
func sanitizeErrorMessage(err error, code int) string {
// 所有 5xx 错误返回通用消息
if code >= 500 {
return "内部服务器错误"
}
// 4xx 错误可以返回具体的业务错误消息
// 但必须使用预定义的错误码和消息,不直接暴露原始错误
if appErr, ok := err.(*errors.AppError); ok {
return appErr.Message
}
// 其他错误返回通用消息
return "请求处理失败"
}
// 错误处理流程
func handleError(c *fiber.Ctx, err error, logger *zap.Logger) error {
// 1. 完整错误记录到日志(包含敏感信息)
logger.Error("请求处理错误",
zap.Error(err),
zap.String("path", c.Path()),
// ... 更多上下文
)
// 2. 返回脱敏的错误消息给客户端
sanitized := sanitizeErrorMessage(err, code)
return c.Status(httpStatus).JSON(Response{
Code: code,
Message: sanitized, // 不包含原始错误详情
// ...
})
}
理由:
- 符合 spec.md FR-007: 隐藏内部实现细节和敏感信息
- 符合 clarification: 为所有错误返回通用消息,原始详情仅记录到日志
- 避免泄露数据库结构、文件路径、堆栈跟踪等敏感信息
- 日志系统已配置访问控制(运维团队可访问),符合安全要求
不采用的方案:
- ❌ 正则表达式过滤敏感信息:复杂、易遗漏、性能开销
- ❌ 白名单机制:维护成本高,容易过时
- ✅ 统一返回通用消息:简单、安全、可靠
4. 响应已发送后的错误处理
研究问题: 当响应已经部分发送给客户端后发生错误,如何处理?
决策: 检测响应状态,已发送则仅记录日志不修改响应
技术方案:
func handleError(c *fiber.Ctx, err error, logger *zap.Logger) error {
// 检查响应是否已发送
if c.Response().StatusCode() != fiber.StatusOK ||
len(c.Response().Body()) > 0 {
// 响应已发送,仅记录日志
logger.Error("响应已发送后发生错误",
zap.Error(err),
zap.Int("status", c.Response().StatusCode()),
zap.Int("body_size", len(c.Response().Body())),
)
return nil // 不再修改响应
}
// 响应未发送,正常处理错误
return buildErrorResponse(c, err)
}
理由:
- 符合 spec.md FR-001 和 edge case: 响应已部分发送时仅记录日志
- 修改已发送的响应会导致响应格式损坏(如 JSON 不完整)
- Fiber 的
c.Response().StatusCode()和c.Response().Body()可以检测发送状态 - 静默失败策略确保不会因错误处理导致更严重的问题
替代方案(不采用):
- ❌ 尝试清空响应重新发送:Fiber 不支持,会导致客户端接收损坏数据
- ❌ 抛出 panic:违反设计原则,应该优雅降级
5. 日志系统集成
研究问题: 如何确保错误处理不因日志系统失败而阻塞请求?
决策: 日志记录采用静默失败策略,日志失败不影响响应
技术方案:
func logError(logger *zap.Logger, fields ...zap.Field) {
defer func() {
if r := recover(); r != nil {
// 日志系统 panic,静默丢弃
// 不记录到任何地方,避免无限循环
}
}()
// 尝试记录日志
logger.Error("错误", fields...)
}
func handleError(c *fiber.Ctx, err error, logger *zap.Logger) error {
// 1. 尝试记录日志(可能失败)
logError(logger,
zap.Error(err),
zap.String("path", c.Path()),
)
// 2. 无论日志是否成功,都继续返回响应
return buildErrorResponse(c, err)
}
理由:
- 符合 spec.md FR-005 clarification: 日志失败时静默处理
- 符合 edge case: 日志系统不可用时丢弃日志,确保请求不受影响
- Zap logger 本身已经有 panic 保护,但显式的 defer/recover 提供额外保障
- 请求处理优先级高于日志记录
现有代码分析:
pkg/logger/logger.go已使用 Zap,支持异步日志internal/middleware/recover.go已正确处理日志记录- 需要确保 ErrorHandler 中的日志调用也采用相同策略
6. 错误分类和 HTTP 状态码映射
研究问题: 如何将不同类型的错误映射到合适的 HTTP 状态码和日志级别?
决策: 基于错误码范围分类,统一映射规则
技术方案:
// pkg/errors/codes.go
const (
// 成功
CodeSuccess = 0
// 客户端错误 (1000-1999) -> 4xx
CodeInvalidParam = 1001 // 400
CodeUnauthorized = 1002 // 401
CodeForbidden = 1003 // 403
CodeNotFound = 1004 // 404
// 服务端错误 (2000-2999) -> 5xx
CodeInternalError = 2001 // 500
CodeDatabaseError = 2002 // 500
CodeServiceUnavailable = 2003 // 503
)
func GetHTTPStatus(code int) int {
switch {
case code >= 1000 && code < 2000:
return mapClientError(code)
case code >= 2000 && code < 3000:
return mapServerError(code)
default:
return 500
}
}
func GetLogLevel(code int) string {
if code >= 2000 {
return "error" // 服务端错误
} else if code >= 1000 {
return "warn" // 客户端错误
}
return "info"
}
理由:
- 符合 spec.md FR-006: 区分客户端和服务端错误
- 错误码范围映射清晰,易于扩展
- 日志级别与错误严重性匹配(客户端错误 = Warn,服务端错误 = Error)
- 便于监控和告警(可以基于错误码范围设置不同的告警策略)
现有代码扩展:
pkg/errors/codes.go需要定义完整的错误码枚举pkg/errors/errors.go已有 AppError 类型,无需修改
7. Request ID 传递
研究问题: 如何在错误响应中关联 Request ID?
决策: Request ID 仅在响应 Header 中传递(X-Request-ID),不在响应体中
技术方案:
func handleError(c *fiber.Ctx, err error, logger *zap.Logger) error {
// 1. 获取 Request ID
requestID := c.Get("X-Request-ID", "")
if requestID == "" {
if rid := c.Locals(constants.ContextKeyRequestID); rid != nil {
requestID = rid.(string)
}
}
// 2. 设置响应 Header
c.Set("X-Request-ID", requestID)
// 3. 日志中包含 Request ID
logger.Error("请求处理错误",
zap.String("request_id", requestID),
zap.Error(err),
)
// 4. 响应体不包含 request_id 字段
return c.Status(httpStatus).JSON(Response{
Code: code,
Message: message,
// 不包含 request_id
})
}
理由:
- 符合 spec.md FR-008 clarification: 不在响应体中包含 request_id
- 通过响应 Header X-Request-ID 传递,客户端可以获取用于追踪
- 日志中包含 request_id,可以关联同一请求的所有日志条目
- 符合 HTTP 标准实践(Request ID 通常在 Header 中)
现有代码集成:
cmd/api/main.go已使用requestid.New()中间件生成 Request IDinternal/middleware/recover.go已从c.Locals()获取 Request ID- ErrorHandler 需要采用相同的获取方式
研究总结
技术栈确认
- 框架: Fiber v2 (已使用)
- 日志: Zap (已使用)
- 错误包: 标准库 errors + 自定义 pkg/errors (已有基础)
- JSON: sonic (已配置)
核心设计决策
- 全局错误处理: 使用 Fiber ErrorHandler + defer/recover 双层保护
- 敏感信息隐藏: 统一返回通用错误消息,原始错误仅记录日志
- 日志策略: 异步日志 + 静默失败,不阻塞请求
- 错误分类: 基于错误码范围映射 HTTP 状态码和日志级别
- Request ID: 通过 Header 传递,不在响应体中
需要实现的组件
| 组件 | 路径 | 描述 |
|---|---|---|
| 错误码定义 | pkg/errors/codes.go | 完整的错误码枚举和消息映射 |
| 全局 ErrorHandler | pkg/errors/handler.go | Fiber ErrorHandler 实现 |
| Recover 中间件增强 | internal/middleware/recover.go | 已有,可能需要小幅调整 |
| 错误辅助函数 | pkg/errors/helpers.go | HTTP 状态码映射、日志级别映射 |
性能考虑
- 错误处理延迟目标: < 1ms
- 使用预分配的错误对象避免频繁内存分配
- 日志记录异步执行(Zap 已支持)
- 避免复杂的字符串处理(如正则匹配)
安全考虑
- 所有 5xx 错误返回通用消息
- 日志访问受限(仅运维团队)
- 堆栈跟踪仅记录到日志,不返回给客户端
- 敏感字段(密码、密钥)不记录到日志
研究完成: ✅ 所有技术不确定性已解决,可以进入设计阶段