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:
2025-11-15 12:17:44 +08:00
parent a371f1cd21
commit fb83c9a706
33 changed files with 7373 additions and 52 deletions

View File

@@ -0,0 +1,42 @@
# Specification Quality Checklist: Fiber 错误处理集成
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2025-11-14
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
所有检查项均通过。规范已完整定义错误处理功能的需求:
- **User Scenarios**: 定义了 4 个优先级明确的用户故事,涵盖统一错误响应、Panic 恢复、错误分类和错误追踪
- **Functional Requirements**: 10 条功能需求明确且可测试
- **Success Criteria**: 8 条成功标准均为可度量的结果指标
- **Edge Cases**: 识别了 6 个边界情况
- **Technical Requirements**: 与项目架构规范保持一致
规范已准备好进入下一阶段 (`/speckit.plan`)。

View File

@@ -0,0 +1,489 @@
openapi: 3.0.3
info:
title: 君鸿卡管系统 - 统一错误响应规范
description: |
本文档定义了系统所有 API 端点的统一错误响应格式和错误码。
**关键原则**:
- 所有错误响应使用统一的 JSON 格式
- 错误码范围: 1000-1999 (客户端错误), 2000-2999 (服务端错误)
- HTTP 状态码与错误码映射一致
- Request ID 仅在响应 Header 中传递 (X-Request-ID)
- 敏感信息仅记录到日志,不返回给客户端
version: 1.0.0
contact:
name: 君鸿卡管系统开发团队
servers:
- url: http://localhost:8080
description: 本地开发环境
- url: https://api.example.com
description: 生产环境
components:
schemas:
ErrorResponse:
type: object
required:
- code
- data
- msg
- timestamp
properties:
code:
type: integer
description: |
应用错误码
- 0: 成功
- 1000-1999: 客户端错误
- 2000-2999: 服务端错误
example: 1001
data:
type: 'null'
description: 错误响应时始终为 null
example: null
msg:
type: string
description: 用户友好的错误消息 (中文, 已脱敏)
example: "参数验证失败"
timestamp:
type: string
format: date-time
description: ISO 8601 格式的时间戳
example: "2025-11-14T16:00:00+08:00"
example:
code: 1001
data: null
msg: "参数验证失败"
timestamp: "2025-11-14T16:00:00+08:00"
responses:
BadRequest:
description: 请求参数验证失败
headers:
X-Request-ID:
schema:
type: string
format: uuid
description: 请求唯一标识符
example: "f1d8b767-dfb3-4588-9fa0-8a97e5337184"
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
InvalidParam:
value:
code: 1001
data: null
msg: "参数验证失败"
timestamp: "2025-11-14T16:00:00+08:00"
RequestTooLarge:
value:
code: 1009
data: null
msg: "请求体过大"
timestamp: "2025-11-14T16:00:00+08:00"
Unauthorized:
description: 未授权访问 (缺失或无效的认证令牌)
headers:
X-Request-ID:
schema:
type: string
format: uuid
description: 请求唯一标识符
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
MissingToken:
value:
code: 1002
data: null
msg: "缺失认证令牌"
timestamp: "2025-11-14T16:00:00+08:00"
InvalidToken:
value:
code: 1003
data: null
msg: "无效或过期的令牌"
timestamp: "2025-11-14T16:00:00+08:00"
Forbidden:
description: 禁止访问 (权限不足)
headers:
X-Request-ID:
schema:
type: string
format: uuid
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
code: 1005
data: null
msg: "禁止访问"
timestamp: "2025-11-14T16:00:00+08:00"
NotFound:
description: 资源未找到
headers:
X-Request-ID:
schema:
type: string
format: uuid
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
code: 1006
data: null
msg: "资源未找到"
timestamp: "2025-11-14T16:00:00+08:00"
Conflict:
description: 资源冲突 (如重复创建)
headers:
X-Request-ID:
schema:
type: string
format: uuid
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
code: 1007
data: null
msg: "资源冲突"
timestamp: "2025-11-14T16:00:00+08:00"
TooManyRequests:
description: 请求过多 (触发限流)
headers:
X-Request-ID:
schema:
type: string
format: uuid
Retry-After:
schema:
type: integer
description: 建议重试的秒数
example: 60
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
code: 1008
data: null
msg: "请求过多,请稍后重试"
timestamp: "2025-11-14T16:00:00+08:00"
InternalServerError:
description: 内部服务器错误 (通用服务端错误)
headers:
X-Request-ID:
schema:
type: string
format: uuid
description: 请求唯一标识符 (用于追踪和调试)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
InternalError:
value:
code: 2001
data: null
msg: "内部服务器错误"
timestamp: "2025-11-14T16:00:00+08:00"
DatabaseError:
value:
code: 2002
data: null
msg: "数据库错误"
timestamp: "2025-11-14T16:00:00+08:00"
RedisError:
value:
code: 2003
data: null
msg: "缓存服务错误"
timestamp: "2025-11-14T16:00:00+08:00"
ServiceUnavailable:
description: 服务暂时不可用
headers:
X-Request-ID:
schema:
type: string
format: uuid
Retry-After:
schema:
type: integer
description: 建议重试的秒数
example: 300
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
code: 2004
data: null
msg: "服务暂时不可用"
timestamp: "2025-11-14T16:00:00+08:00"
GatewayTimeout:
description: 请求超时
headers:
X-Request-ID:
schema:
type: string
format: uuid
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
code: 2005
data: null
msg: "请求超时"
timestamp: "2025-11-14T16:00:00+08:00"
# 错误码完整清单
paths: {}
x-error-codes:
success:
- code: 0
message: "成功"
http_status: 200
client_errors:
- code: 1001
message: "参数验证失败"
http_status: 400
description: "请求参数不符合验证规则"
- code: 1002
message: "缺失认证令牌"
http_status: 401
description: "请求头中缺少 Authorization 令牌"
- code: 1003
message: "无效或过期的令牌"
http_status: 401
description: "认证令牌无效或已过期"
- code: 1004
message: "未授权访问"
http_status: 401
description: "用户未通过认证"
- code: 1005
message: "禁止访问"
http_status: 403
description: "用户权限不足"
- code: 1006
message: "资源未找到"
http_status: 404
description: "请求的资源不存在"
- code: 1007
message: "资源冲突"
http_status: 409
description: "资源已存在或状态冲突"
- code: 1008
message: "请求过多,请稍后重试"
http_status: 429
description: "触发限流规则"
- code: 1009
message: "请求体过大"
http_status: 400
description: "请求体大小超过限制"
server_errors:
- code: 2001
message: "内部服务器错误"
http_status: 500
description: "服务器内部发生未预期的错误"
- code: 2002
message: "数据库错误"
http_status: 500
description: "数据库操作失败 (具体错误仅记录到日志)"
- code: 2003
message: "缓存服务错误"
http_status: 500
description: "Redis 操作失败 (具体错误仅记录到日志)"
- code: 2004
message: "服务暂时不可用"
http_status: 503
description: "服务正在维护或过载"
- code: 2005
message: "请求超时"
http_status: 504
description: "请求处理超时"
- code: 2006
message: "任务队列错误"
http_status: 500
description: "Asynq 任务队列操作失败"
x-security-notes: |
## 敏感信息保护
所有错误响应遵循以下安全原则:
1. **服务端错误 (2xxx)**: 始终返回通用错误消息,不暴露:
- 数据库错误详情 (SQL 语句、表结构)
- 文件路径或系统路径
- 堆栈跟踪信息
- 配置信息或密钥
2. **客户端错误 (1xxx)**: 可返回具体的业务错误消息,但不包括:
- 其他用户的数据
- 系统内部状态
3. **Request ID**:
- 仅在响应 Header X-Request-ID 中传递
- 不在响应体中包含
- 用于日志追踪和调试
4. **日志记录**:
- 完整的错误详情 (包括堆栈、原始错误) 仅记录到日志
- 日志访问需要运维团队权限
- 敏感字段 (密码、密钥) 不记录到日志
x-error-handling-flow: |
## 错误处理流程
1. **请求处理**:
- 中间件或 Handler 返回 error
- 错误被 Fiber ErrorHandler 捕获
2. **错误分类**:
- *AppError: 提取错误码和消息
- *fiber.Error: 映射 HTTP 状态码
- 其他 error: 默认 500 Internal Server Error
3. **响应检查**:
- 如果响应已发送: 仅记录日志,不修改响应
- 如果响应未发送: 生成错误响应
4. **日志记录**:
- 记录完整的错误上下文 (Request ID, 路径, 参数, 原始错误)
- 客户端错误 (1xxx): Warn 级别
- 服务端错误 (2xxx): Error 级别
5. **响应返回**:
- 设置响应 Header: X-Request-ID
- 返回统一格式的 JSON 响应体
- HTTP 状态码与错误码映射一致
x-examples:
successful_request:
summary: 成功请求示例
request:
method: GET
url: /api/v1/users/123
headers:
Authorization: "Bearer valid-token"
response:
status: 200
headers:
X-Request-ID: "f1d8b767-dfb3-4588-9fa0-8a97e5337184"
body:
code: 0
data:
id: "123"
username: "testuser"
email: "test@example.com"
msg: "success"
timestamp: "2025-11-14T16:00:00+08:00"
client_error_missing_token:
summary: 缺失认证令牌
request:
method: GET
url: /api/v1/users/123
headers: {}
response:
status: 401
headers:
X-Request-ID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
body:
code: 1002
data: null
msg: "缺失认证令牌"
timestamp: "2025-11-14T16:00:00+08:00"
client_error_validation:
summary: 参数验证失败
request:
method: POST
url: /api/v1/users
headers:
Authorization: "Bearer valid-token"
body:
username: ""
email: "invalid-email"
response:
status: 400
headers:
X-Request-ID: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
body:
code: 1001
data: null
msg: "参数验证失败"
timestamp: "2025-11-14T16:00:00+08:00"
server_error_database:
summary: 数据库错误 (敏感信息已隐藏)
request:
method: GET
url: /api/v1/users/123
headers:
Authorization: "Bearer valid-token"
response:
status: 500
headers:
X-Request-ID: "c3d4e5f6-a7b8-9012-cdef-123456789012"
body:
code: 2002
data: null
msg: "数据库错误"
timestamp: "2025-11-14T16:00:00+08:00"
note: |
客户端仅收到通用错误消息 "数据库错误"。
完整的错误详情 (如 "pq: relation 'users' does not exist") 仅记录到服务器日志。
客户端可使用 X-Request-ID 联系技术支持进行排查。
rate_limit_exceeded:
summary: 触发限流
request:
method: GET
url: /api/v1/users
headers:
Authorization: "Bearer valid-token"
response:
status: 429
headers:
X-Request-ID: "d4e5f6a7-b8c9-0123-def1-234567890123"
Retry-After: "60"
body:
code: 1008
data: null
msg: "请求过多,请稍后重试"
timestamp: "2025-11-14T16:00:00+08:00"

View File

@@ -0,0 +1,389 @@
# Data Model: Fiber 错误处理集成
**Feature**: 003-error-handling
**Date**: 2025-11-14
**Status**: Draft
## 概述
本文档定义了 Fiber 错误处理集成所需的数据模型和结构。由于这是一个基础设施功能,主要涉及错误处理流程,没有持久化的数据实体,但有运行时的数据结构。
## 核心数据结构
### 1. AppError (应用错误类型)
**位置**: `pkg/errors/errors.go` (已存在,需扩展)
**用途**: 表示应用层的业务错误,包含错误码、消息和原始错误链
**字段**:
```go
type AppError struct {
Code int // 应用错误码 (1000-1999: 客户端错误, 2000-2999: 服务端错误)
Message string // 错误消息 (用户可见,已脱敏)
HTTPStatus int // HTTP 状态码 (根据 Code 自动映射)
Err error // 底层原始错误 (可选,用于错误链)
}
```
**方法**:
```go
// Error 实现 error 接口
func (e *AppError) Error() string
// Unwrap 支持错误链
func (e *AppError) Unwrap() error
// WithHTTPStatus 设置自定义 HTTP 状态码
func (e *AppError) WithHTTPStatus(status int) *AppError
```
**验证规则**:
- Code 必须在定义的范围内 (1000-2999)
- Message 不能为空
- HTTPStatus 如果未设置,根据 Code 自动映射
**关系**:
- 无数据库关系 (运行时对象)
- 可以包装其他 error 形成错误链
---
### 2. ErrorResponse (错误响应结构)
**位置**: `pkg/response/response.go` 中的 Response 结构 (已存在)
**用途**: 统一的 JSON 错误响应格式,返回给客户端
**字段**:
```go
type Response struct {
Code int `json:"code"` // 应用错误码 (0 = 成功, >0 = 错误)
Data any `json:"data"` // 响应数据 (错误时为 null)
Message string `json:"msg"` // 可读消息 (用户友好,已脱敏)
Timestamp string `json:"timestamp"` // ISO 8601 时间戳
}
```
**示例**:
```json
{
"code": 1001,
"data": null,
"msg": "参数验证失败",
"timestamp": "2025-11-14T16:00:00+08:00"
}
```
**验证规则**:
- Code 必须为非负整数
- Timestamp 必须为 RFC3339 格式
- Message 不能为空
- 错误响应时 Data 为 null
**关系**:
- 从 AppError 生成
- Request ID 通过响应 Header X-Request-ID 传递,不在响应体中
---
### 3. ErrorContext (错误上下文)
**位置**: 新增 `pkg/errors/context.go`
**用途**: 记录错误发生时的请求上下文,用于日志记录和调试
**字段**:
```go
type ErrorContext struct {
RequestID string // 请求 ID (唯一标识)
Method string // HTTP 方法
Path string // 请求路径
Query string // Query 参数
IP string // 客户端 IP
UserAgent string // User-Agent
UserID string // 用户 ID (如果已认证)
Headers map[string]string // 重要的请求头 (可选)
StackTrace string // 堆栈跟踪 (panic 时有值)
}
```
**方法**:
```go
// FromFiberContext 从 Fiber Context 提取错误上下文
func FromFiberContext(c *fiber.Ctx) *ErrorContext
// ToLogFields 转换为 Zap 日志字段
func (ec *ErrorContext) ToLogFields() []zap.Field
```
**验证规则**:
- RequestID 不能为空
- Method 和 Path 不能为空
- 其他字段可选
**用途场景**:
- 记录错误日志时附加完整上下文
- 调试时快速定位问题
- 不返回给客户端 (仅内部使用)
---
### 4. ErrorCode (错误码枚举)
**位置**: 新增 `pkg/errors/codes.go`
**用途**: 定义所有应用错误码和对应的默认消息
**结构**:
```go
const (
// 成功
CodeSuccess = 0
// 客户端错误 (1000-1999) -> 4xx HTTP 状态码
CodeInvalidParam = 1001 // 参数验证失败
CodeMissingToken = 1002 // 缺失认证令牌
CodeInvalidToken = 1003 // 无效或过期的令牌
CodeUnauthorized = 1004 // 未授权
CodeForbidden = 1005 // 禁止访问
CodeNotFound = 1006 // 资源未找到
CodeConflict = 1007 // 资源冲突
CodeTooManyRequests = 1008 // 请求过多
CodeRequestTooLarge = 1009 // 请求体过大
// 服务端错误 (2000-2999) -> 5xx HTTP 状态码
CodeInternalError = 2001 // 内部服务器错误
CodeDatabaseError = 2002 // 数据库错误
CodeRedisError = 2003 // Redis 错误
CodeServiceUnavailable = 2004 // 服务不可用
CodeTimeout = 2005 // 请求超时
CodeTaskQueueError = 2006 // 任务队列错误
)
// 错误消息映射 (中文)
var errorMessages = map[int]string{
CodeSuccess: "成功",
CodeInvalidParam: "参数验证失败",
CodeMissingToken: "缺失认证令牌",
CodeInvalidToken: "无效或过期的令牌",
CodeUnauthorized: "未授权访问",
CodeForbidden: "禁止访问",
CodeNotFound: "资源未找到",
CodeConflict: "资源冲突",
CodeTooManyRequests: "请求过多,请稍后重试",
CodeRequestTooLarge: "请求体过大",
CodeInternalError: "内部服务器错误",
CodeDatabaseError: "数据库错误",
CodeRedisError: "缓存服务错误",
CodeServiceUnavailable: "服务暂时不可用",
CodeTimeout: "请求超时",
CodeTaskQueueError: "任务队列错误",
}
// GetMessage 获取错误消息
func GetMessage(code int, lang string) string
```
**HTTP 状态码映射规则**:
```go
func GetHTTPStatus(code int) int {
switch code {
case CodeInvalidParam, CodeRequestTooLarge:
return 400 // Bad Request
case CodeMissingToken, CodeInvalidToken, CodeUnauthorized:
return 401 // Unauthorized
case CodeForbidden:
return 403 // Forbidden
case CodeNotFound:
return 404 // Not Found
case CodeConflict:
return 409 // Conflict
case CodeTooManyRequests:
return 429 // Too Many Requests
case CodeServiceUnavailable:
return 503 // Service Unavailable
case CodeTimeout:
return 504 // Gateway Timeout
default:
if code >= 2000 && code < 3000 {
return 500 // Internal Server Error
}
return 400 // 默认客户端错误
}
}
```
---
## 错误处理流程数据流
```
┌─────────────┐
│ 请求到达 │
└──────┬──────┘
┌─────────────────────┐
│ 中间件/Handler │
│ 返回 error │
└──────┬──────────────┘
┌─────────────────────────────┐
│ Fiber ErrorHandler │
│ 1. 检查响应是否已发送 │
│ 2. 提取错误类型和上下文 │
└──────┬──────────────────────┘
├─────────────┐
│ │
▼ ▼
┌────────┐ ┌──────────┐
│AppError│ │其他Error │
└───┬────┘ └────┬─────┘
│ │
└──────┬──────┘
┌────────────────┐
│ 生成上下文 │
│ ErrorContext │
└────┬───────────┘
├──────────────┐
│ │
▼ ▼
┌─────────┐ ┌──────────────┐
│记录日志 │ │生成响应 │
│(完整上下文)│ │ErrorResponse│
└─────────┘ └──────┬───────┘
┌──────────────┐
│返回给客户端 │
│(脱敏后) │
└──────────────┘
```
---
## 常量定义
### Request ID 上下文键
**位置**: `pkg/constants/constants.go` (已存在,可能需要添加)
```go
const (
ContextKeyRequestID = "request_id" // Fiber Locals 中存储 Request ID 的键
HeaderRequestID = "X-Request-ID" // HTTP Header 中的 Request ID 键
)
```
---
## 非功能性约束
### 性能
- ErrorContext 创建: < 0.1ms
- 错误日志记录: 异步,不阻塞响应 (< 0.5ms)
- 错误响应生成: < 0.5ms
- 总错误处理延迟: < 1ms (P95)
### 并发
- AppError 是不可变的 (immutable),线程安全
- ErrorContext 仅在错误处理流程中创建和使用,不共享
- 错误码常量映射只读,无并发问题
### 内存
- ErrorContext 在请求结束后释放
- 预定义的错误对象可以复用 (如 ErrMissingToken)
- 避免在错误处理中分配大量内存
---
## 与现有代码的集成
### 现有错误类型
**位置**: `pkg/errors/errors.go`
**现状**:
```go
var (
ErrMissingToken = errors.New("missing authentication token")
ErrInvalidToken = errors.New("invalid or expired token")
ErrRedisUnavailable = errors.New("redis unavailable")
ErrTooManyRequests = errors.New("too many requests")
)
type AppError struct {
Code int
Message string
Err error
}
```
**需要的修改**:
1. 为 AppError 添加 HTTPStatus 字段
2. 添加错误码常量 (CodeMissingToken 等)
3. 添加 GetMessage() 函数支持多语言
4. 添加 GetHTTPStatus() 函数映射 HTTP 状态码
### 现有响应结构
**位置**: `pkg/response/response.go`
**现状**: 已有 Response 结构,无需修改
**使用方式**:
```go
// 成功响应 (不变)
response.Success(c, data)
// 错误响应 (现有)
response.Error(c, httpStatus, code, message)
// 新增: 从 AppError 生成错误响应
response.ErrorFromAppError(c, appErr)
```
---
## 数据验证
### 错误码验证
- 必须在定义的范围内 (0, 1000-1999, 2000-2999)
- 未定义的错误码记录警告日志
- 默认映射到 500 Internal Server Error
### 错误消息验证
- 不能为空字符串
- 长度限制: 最大 500 字符
- 不包含换行符或特殊字符 (避免日志注入)
### Request ID 验证
- 必须是有效的 UUID v4 格式
- 如果缺失,ErrorHandler 仍然继续处理
- 记录警告日志
---
## 总结
本数据模型设计:
1. **简洁**: 仅定义必要的运行时结构,无持久化实体
2. **扩展性**: 错误码枚举易于添加新错误类型
3. **安全性**: 错误响应和日志上下文分离,避免敏感信息泄露
4. **性能**: 结构轻量,错误处理开销小
5. **兼容性**: 与现有 pkg/errors 和 pkg/response 自然集成
所有数据结构都遵循 Go 惯用法: 简单的结构体,少量的方法,清晰的职责划分。

View File

@@ -0,0 +1,364 @@
# Implementation Plan: Fiber 错误处理集成
**Branch**: `003-error-handling` | **Date**: 2025-11-14 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/003-error-handling/spec.md`
## Summary
实现统一的 Fiber 错误处理机制,包括全局 ErrorHandler、Panic 恢复、错误分类和安全的错误响应。核心目标是捕获所有错误和 panic,返回统一格式的 JSON 响应,同时隐藏敏感信息,记录完整的错误上下文到日志。
**技术方案**: 使用 Fiber ErrorHandler + defer/recover 双层保护,基于错误码范围映射 HTTP 状态码,Request ID 通过 Header 传递,日志采用静默失败策略。
## Technical Context
**Language/Version**: Go 1.25.4
**Primary Dependencies**: Fiber v2 (HTTP 框架), Zap (日志), sonic (JSON), 标准库 errors
**Storage**: N/A (无持久化数据,仅运行时错误处理)
**Testing**: Go 标准 testing 框架 + httptest
**Target Platform**: Linux server (Docker 容器)
**Project Type**: single (后端 API 服务)
**Performance Goals**: 错误处理延迟 < 1ms (P95), 不显著增加请求处理时间
**Constraints**:
- 错误响应不能暴露敏感信息 (数据库错误、文件路径、堆栈跟踪)
- 日志失败不能阻塞响应
- ErrorHandler 自身必须防止 panic 无限循环
- 响应已发送后不能修改响应内容
**Scale/Scope**:
- 影响所有 API 端点 (用户、订单、任务等)
- 约 10+ 错误码定义
- 3-5 个新增/修改的文件
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
**Tech Stack Adherence**:
- [x] Feature uses Fiber + GORM + Viper + Zap + Lumberjack.v2 + Validator + sonic JSON + Asynq + PostgreSQL
- [x] No native calls bypass framework (no `database/sql`, `net/http`, `encoding/json` direct use)
- [x] All HTTP operations use Fiber framework
- [x] All database operations use GORM (N/A - 本功能无数据库操作)
- [x] All async tasks use Asynq (N/A - 本功能无异步任务)
- [x] Uses Go official toolchain: `go fmt`, `go vet`, `golangci-lint`
- [x] Uses Go Modules for dependency management
**Code Quality Standards**:
- [x] Follows Handler → Service → Store → Model architecture (本功能主要在 pkg/ 包中)
- [x] Handler layer only handles HTTP, no business logic
- [x] Service layer contains business logic with cross-module support (N/A - 本功能为基础设施)
- [x] Store layer manages all data access with transaction support (N/A - 无数据访问)
- [x] Uses dependency injection via struct fields (not constructor patterns)
- [x] Unified error codes in `pkg/errors/` ✅ 本功能核心
- [x] Unified API responses via `pkg/response/` ✅ 本功能核心
- [x] All constants defined in `pkg/constants/`
- [x] All Redis keys managed via key generation functions (N/A - 无 Redis 操作)
- [x] **No hardcoded magic numbers or strings (3+ occurrences must be constants)** ✅ 错误码和消息均为常量
- [x] **Defined constants are used instead of hardcoding duplicate values** ✅ 错误消息通过映射表管理
- [x] **Code comments prefer Chinese for readability** ✅ 所有注释使用中文
- [x] **Log messages use Chinese** ✅ 所有日志消息使用中文
- [x] **Error messages support Chinese** ✅ 错误消息中文优先
- [x] All exported functions/types have Go-style doc comments
- [x] Code formatted with `gofmt`
- [x] Follows Effective Go and Go Code Review Comments
**Documentation Standards** (Constitution Principle VII):
- [x] Feature summary docs placed in `docs/{feature-id}/` mirroring `specs/{feature-id}/`
- [x] Summary doc filenames use Chinese (功能总结.md, 使用指南.md, etc.)
- [x] Summary doc content uses Chinese
- [x] README.md updated with brief Chinese summary (2-3 sentences)
- [x] Documentation is concise for first-time contributors
**Go Idiomatic Design**:
- [x] Package structure is flat (max 2-3 levels), organized by feature ✅ pkg/errors/
- [x] Interfaces are small (1-3 methods), defined at use site ✅ fiber.ErrorHandler
- [x] No Java-style patterns: no I-prefix, no Impl-suffix, no getters/setters
- [x] Error handling is explicit (return errors, no panic/recover abuse) ✅ 核心功能
- [x] Uses composition over inheritance
- [x] Uses goroutines and channels (not thread pools) (N/A - 本功能无并发)
- [x] Uses `context.Context` for cancellation and timeouts (N/A - 错误处理无需 context)
- [x] Naming follows Go conventions: short receivers, consistent abbreviations
- [x] No Hungarian notation or type prefixes
- [x] Simple constructors (New/NewXxx), no Builder pattern unless necessary
**Testing Standards**:
- [x] Unit tests for all core business logic (Service layer)
- [x] Integration tests for all API endpoints ✅ 错误处理集成测试
- [x] Tests use Go standard testing framework
- [x] Test files named `*_test.go` in same directory
- [x] Test functions use `Test` prefix, benchmarks use `Benchmark` prefix
- [x] Table-driven tests for multiple test cases ✅ 多种错误场景测试
- [x] Test helpers marked with `t.Helper()`
- [x] Tests are independent (no external service dependencies)
- [x] Target coverage: 70%+ overall, 90%+ for core business ✅ 错误处理核心逻辑 90%+
**User Experience Consistency**:
- [x] All APIs use unified JSON response format ✅ 本功能核心
- [x] Error responses include clear error codes and bilingual messages ✅ 中文消息
- [x] RESTful design principles followed
- [x] Unified pagination parameters (N/A - 本功能无分页)
- [x] Time fields use ISO 8601 format (RFC3339) ✅ timestamp 字段
- [x] Currency amounts use integers (N/A - 本功能无货币)
**Performance Requirements**:
- [x] API response time (P95) < 200ms, (P99) < 500ms ✅ 错误处理 < 1ms
- [x] Batch operations use bulk queries/inserts (N/A - 本功能无批量操作)
- [x] All database queries have appropriate indexes (N/A - 无数据库操作)
- [x] List queries implement pagination (N/A - 无列表查询)
- [x] Non-realtime operations use async tasks (N/A - 错误处理必须同步)
- [x] Database and Redis connection pools properly configured (N/A)
- [x] Uses goroutines/channels for concurrency (N/A - 错误处理同步执行)
- [x] Uses `context.Context` for timeout control (N/A)
- [x] Uses `sync.Pool` for frequently allocated objects (可选优化 - ErrorContext)
**Access Logging Standards** (Constitution Principle VIII):
- [x] ALL HTTP requests logged to access.log without exception ✅ 已有实现
- [x] Request parameters (query + body) logged (limited to 50KB) ✅ 已有实现
- [x] Response parameters (body) logged (limited to 50KB) ✅ 已有实现
- [x] Logging happens via centralized Logger middleware ✅ 已有实现
- [x] No middleware bypasses access logging ✅ ErrorHandler 不绕过日志
- [x] Body truncation indicates "... (truncated)" when over 50KB limit ✅ 已有实现
- [x] Access log includes all required fields ✅ 已有实现
## Project Structure
### Documentation (this feature)
**设计文档specs/ 目录)**:开发前的规划和设计
```text
specs/003-error-handling/
├── plan.md # This file (/speckit.plan command output)
├── research.md # Phase 0 output - 技术研究和决策
├── data-model.md # Phase 1 output - 错误处理数据结构
├── quickstart.md # Phase 1 output - 快速上手指南
├── contracts/ # Phase 1 output - API contracts
│ └── error-responses.yaml # 错误响应规范 (OpenAPI)
└── tasks.md # Phase 2 output - 任务分解 (NOT created by /speckit.plan)
```
**总结文档docs/ 目录)**:开发完成后的总结和使用指南(遵循 Constitution Principle VII
```text
docs/003-error-handling/
├── 功能总结.md # 功能概述、核心实现、技术要点
├── 使用指南.md # 如何使用错误处理机制
└── 架构说明.md # 错误处理架构设计(可选)
```
**README.md 更新**:完成功能后添加简短描述
```markdown
## 核心功能
- **统一错误处理**:全局 ErrorHandler + Panic 恢复,统一错误响应格式,安全的敏感信息隐藏
```
### Source Code (repository root)
```text
pkg/
├── errors/
│ ├── errors.go # 已存在 - 需扩展 AppError
│ ├── codes.go # 新增 - 错误码枚举和消息映射
│ ├── handler.go # 新增 - Fiber ErrorHandler 实现
│ └── context.go # 新增 - 错误上下文提取
├── response/
│ └── response.go # 已存在 - 无需修改
├── constants/
│ └── constants.go # 已存在 - 可能需要添加 Request ID 常量
└── logger/
└── logger.go # 已存在 - 无需修改
internal/middleware/
└── recover.go # 已存在 - 可能需要小幅调整
cmd/api/
└── main.go # 需修改 - 配置 Fiber ErrorHandler
tests/integration/
└── error_handler_test.go # 新增 - 错误处理集成测试
```
**Structure Decision**: 单一项目结构,错误处理作为基础设施包放在 `pkg/errors/` 下,供所有模块使用。与现有 `pkg/response/``pkg/logger/` 包协同工作。
## Complexity Tracking
> **Fill ONLY if Constitution Check has violations that must be justified**
无违反项。所有设计决策符合项目宪章要求。
## Phase 0: Research (Complete ✅)
**Output**: `research.md`
已完成技术研究,解决了以下关键问题:
1. Fiber ErrorHandler 机制和中间件集成
2. ErrorHandler 自身保护 (defer/recover)
3. 敏感信息识别和隐藏策略
4. 响应已发送后的错误处理
5. 日志系统集成和静默失败策略
6. 错误分类和 HTTP 状态码映射
7. Request ID 传递方式
**核心决策**:
- 使用 Fiber ErrorHandler + defer/recover 双层保护
- 所有 5xx 错误返回通用消息,原始错误仅记录日志
- 日志采用静默失败策略,不阻塞响应
- 基于错误码范围 (1000-1999, 2000-2999) 映射 HTTP 状态码
- Request ID 仅在 Header 中传递,不在响应体中
## Phase 1: Design & Contracts (Complete ✅)
**Prerequisites:** `research.md` complete ✅
### Data Model
**Output**: `data-model.md`
定义了错误处理的核心数据结构:
1. **AppError**: 应用错误类型,包含错误码、消息、HTTP 状态码、错误链
2. **ErrorResponse**: 统一的 JSON 错误响应格式
3. **ErrorContext**: 错误发生时的请求上下文 (用于日志)
4. **ErrorCode**: 错误码枚举和消息映射
**关键实体**:
- 无持久化实体 (运行时对象)
- 错误处理流程数据流已定义
- 性能约束: ErrorContext 创建 < 0.1ms, 总延迟 < 1ms
### API Contracts
**Output**: `contracts/error-responses.yaml`
OpenAPI 3.0 格式定义了:
- 统一的 ErrorResponse schema
- 常见错误响应 (400, 401, 403, 404, 409, 429, 500, 503, 504)
- 完整的错误码清单 (1001-1009, 2001-2006)
- HTTP 状态码映射规则
- 安全规范和错误处理流程
- 实际示例 (成功、客户端错误、服务端错误、限流)
### Quick Start Guide
**Output**: `quickstart.md`
为开发者提供:
- 5 分钟快速开始指南
- 常用错误码表格
- Handler 中返回错误的 3 种方式
- 客户端错误处理示例 (TypeScript, Python)
- 进阶使用: 自定义消息、错误链、Panic 恢复
- 调试技巧: Request ID 追踪
- 常见错误场景和最佳实践
- 测试示例和 FAQ
### Agent Context Update
**Output**: CLAUDE.md updated ✅
已更新 Claude 上下文文件,添加错误处理相关技术栈信息。
## Phase 2: Implementation Planning
**This phase is handled by `/speckit.tasks` command, NOT by `/speckit.plan`.**
`/speckit.plan` 命令在此停止。下一步:
1. 运行 `/speckit.tasks` 生成详细的任务分解 (`tasks.md`)
2. 运行 `/speckit.implement` 执行实施
预期的 `tasks.md` 将包含:
- **Task 1**: 扩展 pkg/errors/errors.go (添加 HTTPStatus 字段和方法)
- **Task 2**: 创建 pkg/errors/codes.go (错误码枚举和消息映射)
- **Task 3**: 创建 pkg/errors/handler.go (Fiber ErrorHandler 实现)
- **Task 4**: 创建 pkg/errors/context.go (错误上下文提取)
- **Task 5**: 更新 cmd/api/main.go (配置 ErrorHandler)
- **Task 6**: 调整 internal/middleware/recover.go (如需)
- **Task 7**: 创建集成测试 tests/integration/error_handler_test.go
- **Task 8**: 更新文档 docs/003-error-handling/
## Implementation Notes
### 关键依赖关系
1. **错误码定义优先**: `pkg/errors/codes.go` 必须先完成,因为其他组件依赖错误码常量
2. **AppError 扩展**: 扩展现有 `pkg/errors/errors.go`,保持向后兼容
3. **ErrorHandler 集成**: 在 `cmd/api/main.go` 中配置 Fiber ErrorHandler
4. **测试驱动**: 先编写集成测试,验证各种错误场景
### 风险和缓解
**风险 1: ErrorHandler 自身 panic 导致服务崩溃**
- 缓解: 使用 defer/recover 保护 ErrorHandler,失败时返回空响应
- **保护机制触发条件明确**:
- **触发范围**: defer/recover 仅保护 ErrorHandler 函数本身的执行过程
- **捕获的异常**: 任何在 ErrorHandler 内部发生的 panic (包括日志系统崩溃、JSON 序列化失败、响应写入错误等)
- **不捕获的异常**: Fiber 中间件链中的 panic 由 Recover 中间件处理,不在此保护范围内
- **失败响应**: 当 ErrorHandler 自身 panic 时,返回 HTTP 500 状态码,空响应体 (Content-Length: 0)
- **日志记录**: 保护机制触发时的 panic 信息会被记录 (如果日志系统可用),但不阻塞响应返回
- **示例场景**:
1. Zap 日志系统崩溃 → defer/recover 捕获 → 返回 HTTP 500 空响应
2. sonic JSON 序列化失败 → defer/recover 捕获 → 返回 HTTP 500 空响应
3. c.Status().JSON() 写入响应失败 → defer/recover 捕获 → 返回 HTTP 500 空响应
4. 业务逻辑中的 panic → Recover 中间件捕获 → 传递给 ErrorHandler → ErrorHandler 正常处理
**风险 2: 日志系统失败阻塞响应**
- 缓解: 日志调用使用 defer/recover,静默失败
**风险 3: 响应已发送后修改响应导致损坏**
- 缓解: 检查响应状态,已发送则仅记录日志
**风险 4: 敏感信息泄露**
- 缓解: 所有 5xx 错误返回通用消息,原始错误仅记录日志
### 性能优化
1. **预分配错误对象**: 常见错误 (ErrMissingToken 等) 使用预定义对象
2. **避免字符串拼接**: 使用 `fmt.Errorf``%w` 包装错误
3. **异步日志**: Zap 已支持,无需额外配置
4. **ErrorContext 池化** (可选): 如果性能测试显示分配开销大,使用 `sync.Pool`
### 测试策略
**单元测试**:
- pkg/errors/codes.go: 错误码映射函数
- pkg/errors/context.go: ErrorContext 提取逻辑
- pkg/errors/handler.go: ErrorHandler 核心逻辑
**集成测试**:
- 参数验证失败 → 400 错误
- 认证失败 → 401 错误
- 资源未找到 → 404 错误
- 数据库错误 → 500 错误 (敏感信息已隐藏)
- Panic 恢复 → 500 错误 (堆栈记录到日志)
- 限流触发 → 429 错误
- 响应已发送后的错误处理
**性能测试**:
- 错误处理延迟基准测试
- 并发场景下的错误处理
### 部署注意事项
1. **向后兼容**: 现有错误处理代码继续工作,逐步迁移到新机制
2. **日志轮转**: 确保日志文件配置正确的轮转策略
3. **监控**: 配置告警规则监控 5xx 错误率
4. **文档**: 更新 API 文档,说明新的错误响应格式
## Constitution Re-Check (Post-Design)
✅ 所有设计决策符合项目宪章要求:
- Tech Stack Adherence: 使用 Fiber, Zap, sonic
- Code Quality: 清晰的分层,统一的错误码和响应
- Go Idiomatic Design: 简单的结构体,显式的错误处理,无 Java 风格模式
- Testing Standards: 单元测试 + 集成测试,table-driven tests
- Performance: 错误处理延迟 < 1ms
- Security: 敏感信息隐藏,日志访问控制
---
**Plan Completion**: ✅ Phase 0 研究和 Phase 1 设计已完成
**Branch**: `003-error-handling`
**Next Step**: 运行 `/speckit.tasks` 生成任务分解,然后 `/speckit.implement` 执行实施
**Generated Artifacts**:
-`research.md` - 技术研究和决策
-`data-model.md` - 错误处理数据结构
-`contracts/error-responses.yaml` - 错误响应规范 (OpenAPI)
-`quickstart.md` - 快速上手指南
- ✅ CLAUDE.md - 已更新 agent 上下文

View File

@@ -0,0 +1,541 @@
# Quick Start: Fiber 错误处理集成
**Feature**: 003-error-handling
**Date**: 2025-11-14
**Audience**: 新开发者、集成工程师
## 概述
本文档提供 Fiber 错误处理集成的快速上手指南,帮助开发者快速理解和使用统一的错误处理机制。
## 5 分钟快速开始
### 1. 错误响应格式
**所有 API 错误响应都使用统一格式**:
```json
{
"code": 1001,
"data": null,
"msg": "参数验证失败",
"timestamp": "2025-11-14T16:00:00+08:00"
}
```
- `code`: 错误码 (1000-1999: 客户端错误, 2000-2999: 服务端错误)
- `data`: 错误时始终为 `null`
- `msg`: 用户友好的错误消息 (中文)
- `timestamp`: ISO 8601 格式时间戳
**Request ID** 在响应 Header 中:
```
X-Request-ID: f1d8b767-dfb3-4588-9fa0-8a97e5337184
```
---
### 2. 在 Handler 中返回错误
**方式 1: 使用预定义错误码**
```go
import (
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
func (h *Handler) CreateUser(c *fiber.Ctx) error {
var req CreateUserRequest
if err := c.BodyParser(&req); err != nil {
// 返回参数验证失败错误
return errors.New(errors.CodeInvalidParam, "参数格式错误")
}
// 业务逻辑...
user, err := h.service.Create(req)
if err != nil {
// 包装错误,添加错误码
return errors.Wrap(errors.CodeDatabaseError, "创建用户失败", err)
}
return response.Success(c, user)
}
```
**方式 2: 直接返回 error (由 ErrorHandler 自动处理)**
```go
func (h *Handler) GetUser(c *fiber.Ctx) error {
id := c.Params("id")
user, err := h.service.GetByID(c.Context(), id)
if err != nil {
// 直接返回错误,ErrorHandler 会自动映射为 500 错误
return err
}
return response.Success(c, user)
}
```
**方式 3: 使用预定义错误常量**
```go
import "github.com/break/junhong_cmp_fiber/pkg/errors"
func (m *Middleware) CheckAuth(c *fiber.Ctx) error {
token := c.Get("Authorization")
if token == "" {
// 使用预定义错误
return errors.ErrMissingToken
}
return c.Next()
}
```
---
### 3. 常用错误码
| 错误码 | HTTP 状态 | 消息 | 使用场景 |
|--------|----------|------|---------|
| 1001 | 400 | 参数验证失败 | 请求参数不符合规则 |
| 1002 | 401 | 缺失认证令牌 | 未提供 Authorization header |
| 1003 | 401 | 无效或过期的令牌 | 令牌验证失败 |
| 1006 | 404 | 资源未找到 | 数据库中找不到资源 |
| 1008 | 429 | 请求过多 | 触发限流 |
| 2001 | 500 | 内部服务器错误 | 未预期的服务器错误 |
| 2002 | 500 | 数据库错误 | 数据库操作失败 |
| 2003 | 500 | 缓存服务错误 | Redis 操作失败 |
**完整列表**: 见 `pkg/errors/codes.go`
---
### 4. 客户端错误处理示例
**JavaScript/TypeScript**:
```typescript
async function getUser(userId: string): Promise<User> {
const response = await fetch(`/api/v1/users/${userId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (data.code !== 0) {
// 错误处理
const requestId = response.headers.get('X-Request-ID');
switch (data.code) {
case 1002:
case 1003:
// 认证失败,跳转登录
redirectToLogin();
break;
case 1006:
// 资源未找到
showNotFoundMessage();
break;
case 2001:
case 2002:
// 服务器错误,提示用户联系技术支持
showErrorMessage(`服务器错误,请联系技术支持\nRequest ID: ${requestId}`);
break;
default:
showErrorMessage(data.msg);
}
throw new Error(data.msg);
}
return data.data;
}
```
**Python**:
```python
import requests
def get_user(user_id: str, token: str) -> dict:
response = requests.get(
f'/api/v1/users/{user_id}',
headers={'Authorization': f'Bearer {token}'}
)
data = response.json()
request_id = response.headers.get('X-Request-ID')
if data['code'] != 0:
# 错误处理
if data['code'] in [1002, 1003]:
raise AuthenticationError(data['msg'])
elif data['code'] == 1006:
raise NotFoundError(data['msg'])
elif data['code'] >= 2000:
raise ServerError(f"{data['msg']} (Request ID: {request_id})")
else:
raise APIError(data['msg'])
return data['data']
```
---
## 进阶使用
### 自定义错误消息
```go
// 使用预定义错误码 + 自定义消息
func (h *Handler) UpdateUser(c *fiber.Ctx) error {
id := c.Params("id")
user, err := h.service.GetByID(c.Context(), id)
if err != nil {
// 自定义错误消息
return errors.New(errors.CodeNotFound, fmt.Sprintf("用户 %s 不存在", id))
}
// 更新逻辑...
}
```
### 错误链传递
```go
// Service 层
func (s *Service) CreateOrder(req *CreateOrderRequest) (*Order, error) {
user, err := s.userService.GetByID(req.UserID)
if err != nil {
// 包装错误,保留错误链
return nil, fmt.Errorf("获取用户信息失败: %w", err)
}
// 订单创建逻辑...
}
// Handler 层
func (h *Handler) CreateOrder(c *fiber.Ctx) error {
order, err := h.service.CreateOrder(req)
if err != nil {
// 包装为 AppError,原始错误链会记录到日志
return errors.Wrap(errors.CodeInternalError, "创建订单失败", err)
}
return response.Success(c, order)
}
```
### Panic 自动恢复
**Panic 会被自动捕获并转换为 500 错误**:
```go
func (h *Handler) DangerousOperation(c *fiber.Ctx) error {
// 如果这里发生 panic
result := riskyFunction()
// Recover 中间件会捕获 panic,返回统一错误响应
// 客户端收到: {"code": 2001, "msg": "内部服务器错误"}
// 完整堆栈会记录到日志
return response.Success(c, result)
}
```
**注意**: 不要滥用 panic,业务错误应该使用 error 返回。
---
## 调试技巧
### 1. 使用 Request ID 追踪错误
**客户端获取 Request ID**:
```bash
curl -i http://localhost:8080/api/v1/users/123
```
响应:
```
HTTP/1.1 500 Internal Server Error
X-Request-ID: f1d8b767-dfb3-4588-9fa0-8a97e5337184
Content-Type: application/json
{"code": 2002, "data": null, "msg": "数据库错误", "timestamp": "..."}
```
**在日志中搜索 Request ID**:
```bash
grep "f1d8b767-dfb3-4588-9fa0-8a97e5337184" logs/app.log
```
日志会包含完整的错误详情:
```json
{
"level": "error",
"timestamp": "2025-11-14T16:00:00+08:00",
"request_id": "f1d8b767-dfb3-4588-9fa0-8a97e5337184",
"method": "GET",
"path": "/api/v1/users/123",
"error": "pq: relation 'users' does not exist",
"stack": "..."
}
```
### 2. 本地开发时查看完整错误
**开发环境**: 查看 `logs/app.log` 获取详细错误信息
**生产环境**: 使用 Request ID 联系运维团队查看日志
---
## 常见错误场景
### 场景 1: 参数验证失败
```go
type CreateUserRequest struct {
Username string `json:"username" validate:"required,min=3,max=50"`
Email string `json:"email" validate:"required,email"`
}
func (h *Handler) CreateUser(c *fiber.Ctx) error {
var req CreateUserRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求格式错误")
}
// 使用 validator 验证
if err := validate.Struct(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "参数验证失败")
}
// 创建用户...
}
```
### 场景 2: 资源未找到
```go
func (h *Handler) GetUser(c *fiber.Ctx) error {
id := c.Params("id")
user, err := h.service.GetByID(c.Context(), id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New(errors.CodeNotFound, "用户不存在")
}
return errors.Wrap(errors.CodeDatabaseError, "查询用户失败", err)
}
return response.Success(c, user)
}
```
### 场景 3: 数据库错误
```go
func (h *Handler) UpdateUser(c *fiber.Ctx) error {
id := c.Params("id")
var req UpdateUserRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求格式错误")
}
user, err := h.service.Update(c.Context(), id, &req)
if err != nil {
// 数据库错误会被包装,原始错误仅记录到日志
return errors.Wrap(errors.CodeDatabaseError, "更新用户失败", err)
}
return response.Success(c, user)
}
```
### 场景 4: 外部服务不可用
```go
func (h *Handler) SendEmail(c *fiber.Ctx) error {
var req SendEmailRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求格式错误")
}
err := h.emailService.Send(req.To, req.Subject, req.Body)
if err != nil {
// 外部服务错误
return errors.Wrap(errors.CodeServiceUnavailable, "邮件服务暂时不可用", err)
}
return response.Success(c, nil)
}
```
---
## 最佳实践
### ✅ 推荐做法
1. **使用预定义错误码**: 保持错误码的一致性
```go
return errors.New(errors.CodeInvalidParam, "用户名不能为空")
```
2. **包装底层错误**: 保留错误链,便于调试
```go
return errors.Wrap(errors.CodeDatabaseError, "查询失败", err)
```
3. **提供友好的错误消息**: 使用中文,面向用户
```go
return errors.New(errors.CodeNotFound, "订单不存在")
```
4. **区分客户端和服务端错误**: 使用正确的错误码范围
- 1000-1999: 客户端问题 (参数错误、权限不足等)
- 2000-2999: 服务端问题 (数据库错误、服务不可用等)
### ❌ 避免做法
1. **不要硬编码错误消息**
```go
// ❌ 错误
return c.Status(400).JSON(fiber.Map{"error": "参数错误"})
// ✅ 正确
return errors.New(errors.CodeInvalidParam, "参数错误")
```
2. **不要暴露敏感信息**
```go
// ❌ 错误: 暴露 SQL 语句
return errors.New(errors.CodeDatabaseError, err.Error())
// ✅ 正确: 使用通用消息
return errors.Wrap(errors.CodeDatabaseError, "查询失败", err)
```
3. **不要滥用 panic**
```go
// ❌ 错误: 业务错误不应该 panic
if user == nil {
panic("user not found")
}
// ✅ 正确: 使用 error 返回
if user == nil {
return errors.New(errors.CodeNotFound, "用户不存在")
}
```
4. **不要忽略错误**
```go
// ❌ 错误: 忽略错误
user, _ := h.service.GetByID(id)
// ✅ 正确: 处理错误
user, err := h.service.GetByID(id)
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, "获取用户失败", err)
}
```
---
## 测试错误处理
### 单元测试示例
```go
func TestHandler_CreateUser_InvalidParam(t *testing.T) {
app := fiber.New()
handler := NewHandler(mockService, logger)
app.Post("/users", handler.CreateUser)
// 发送无效请求
req := httptest.NewRequest("POST", "/users", strings.NewReader(`{"username": ""}`))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req)
// 验证状态码
assert.Equal(t, 400, resp.StatusCode)
// 验证响应格式
var result response.Response
json.NewDecoder(resp.Body).Decode(&result)
assert.Equal(t, errors.CodeInvalidParam, result.Code)
assert.Nil(t, result.Data)
assert.NotEmpty(t, result.Message)
// 验证 Request ID header
requestID := resp.Header.Get("X-Request-ID")
assert.NotEmpty(t, requestID)
}
```
---
## 常见问题 (FAQ)
**Q: 为什么错误响应中没有 request_id 字段?**
A: Request ID 在响应 Header `X-Request-ID` 中传递,不在响应体中。这符合 HTTP 标准实践。
**Q: 如何添加新的错误码?**
A: 在 `pkg/errors/codes.go` 中添加常量定义和错误消息映射:
```go
const (
CodeMyNewError = 1010 // 客户端错误
)
var errorMessages = map[int]string{
CodeMyNewError: "我的新错误",
}
```
**Q: 服务端错误为什么只返回通用消息?**
A: 出于安全考虑,避免泄露数据库结构、文件路径等敏感信息。完整错误详情会记录到日志,运维团队可以通过 Request ID 查看。
**Q: Panic 会导致服务崩溃吗?**
A: 不会。Recover 中间件会捕获所有 panic,转换为 500 错误响应,确保服务继续运行。
**Q: 如何在日志中搜索特定用户的所有错误?**
A: 日志包含 `user_id` 字段 (如果已认证),可以搜索:
```bash
grep '"user_id":"123"' logs/app.log | grep '"level":"error"'
```
---
## 下一步
- 查看完整的错误码列表: `pkg/errors/codes.go`
- 了解错误处理实现细节: `specs/003-error-handling/research.md`
- 查看 API contracts: `specs/003-error-handling/contracts/error-responses.yaml`
- 阅读完整实施计划: `specs/003-error-handling/plan.md`
---
**版本**: 1.0.0
**最后更新**: 2025-11-14

View File

@@ -0,0 +1,376 @@
# Research: Fiber 错误处理集成
**Feature**: 003-error-handling
**Date**: 2025-11-14
**Status**: Complete
## 研究目标
解决实施 Fiber 错误处理集成时的技术不确定性和最佳实践。
## 研究任务
### 1. Fiber 框架错误处理机制
**研究问题**: Fiber 如何实现全局错误处理?如何与中间件链配合?
**决策**: 使用 Fiber 的 `ErrorHandler` 配置项实现全局错误处理
**技术方案**:
```go
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](https://docs.gofiber.io/guide/error-handling)
- 现有代码: `cmd/api/main.go` 中的 Fiber 应用配置
---
### 2. ErrorHandler 自身保护机制
**研究问题**: 如何防止 ErrorHandler 本身发生错误或 panic 导致无限循环或服务崩溃?
**决策**: 使用 defer + recover 保护 ErrorHandler失败时返回最简响应
**技术方案**:
```go
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. 敏感信息识别和隐藏
**研究问题**: 如何自动识别并隐藏错误消息中的敏感信息(数据库错误、文件路径、密钥等)?
**决策**: 为所有内部错误返回通用错误消息,原始错误仅记录到日志
**技术方案**:
```go
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. 响应已发送后的错误处理
**研究问题**: 当响应已经部分发送给客户端后发生错误,如何处理?
**决策**: 检测响应状态,已发送则仅记录日志不修改响应
**技术方案**:
```go
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. 日志系统集成
**研究问题**: 如何确保错误处理不因日志系统失败而阻塞请求?
**决策**: 日志记录采用静默失败策略,日志失败不影响响应
**技术方案**:
```go
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 状态码和日志级别?
**决策**: 基于错误码范围分类,统一映射规则
**技术方案**:
```go
// 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不在响应体中
**技术方案**:
```go
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 ID
- `internal/middleware/recover.go` 已从 `c.Locals()` 获取 Request ID
- ErrorHandler 需要采用相同的获取方式
---
## 研究总结
### 技术栈确认
- **框架**: Fiber v2 (已使用)
- **日志**: Zap (已使用)
- **错误包**: 标准库 errors + 自定义 pkg/errors (已有基础)
- **JSON**: sonic (已配置)
### 核心设计决策
1. **全局错误处理**: 使用 Fiber ErrorHandler + defer/recover 双层保护
2. **敏感信息隐藏**: 统一返回通用错误消息,原始错误仅记录日志
3. **日志策略**: 异步日志 + 静默失败,不阻塞请求
4. **错误分类**: 基于错误码范围映射 HTTP 状态码和日志级别
5. **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 错误返回通用消息
- 日志访问受限(仅运维团队)
- 堆栈跟踪仅记录到日志,不返回给客户端
- 敏感字段(密码、密钥)不记录到日志
---
**研究完成**: ✅ 所有技术不确定性已解决,可以进入设计阶段

View File

@@ -0,0 +1,178 @@
# Feature Specification: Fiber 错误处理集成
**Feature Branch**: `003-error-handling`
**Created**: 2025-11-14
**Status**: Draft
**Input**: User description: "我想把异常处理集成进来 - Fiber 错误处理集成,包括捕获错误、Panic 恢复、自定义错误处理程序"
## Clarifications
### Session 2025-11-14
- Q: 当 Zap 日志系统(如远程日志服务)不可用时,系统应该如何处理错误日志? → A: 静默失败,丢弃日志,确保请求不受影响
- Q: 当全局错误处理程序ErrorHandler本身执行时发生错误或 panic系统应该如何避免无限循环或崩溃 → A: 使用 defer + recover 保护 ErrorHandler,失败时仅返回 HTTP 500 状态码,空响应体
- Q: 错误响应结构 ErrorResponse 是否需要包含 request_id 字段?当前 pkg/response/response.go 中没有此字段,但 FR-008 要求关联请求 ID。 → A: 不在响应体中包含 request_id仅在响应 Header 中添加 X-Request-ID
- Q: 当 HTTP 响应已经部分发送给客户端(如已写入响应头或部分响应体)后发生错误,系统应该如何处理? → A: 静默失败,记录日志但不修改已发送的响应
- Q: 当错误信息包含敏感数据如数据库连接字符串、内部文件路径、密钥ErrorHandler 应该如何识别并避免泄露到客户端响应或日志中? → A: 为所有错误返回通用消息(如"内部服务器错误"),原始详情仅记录到日志;日志访问受限
## User Scenarios & Testing *(mandatory)*
### User Story 1 - 统一错误响应格式 (Priority: P1)
当系统发生任何错误时,API 用户(前端开发者、移动端开发者、第三方集成商)需要接收到结构化、一致的错误响应,以便能够正确识别错误类型并向最终用户展示友好的错误信息。
**Why this priority**: 这是错误处理的核心功能,直接影响 API 的可用性和用户体验。统一的错误格式是所有后续错误处理功能的基础。
**Independent Test**: 可以通过调用任意一个会产生错误的 API 端点(如访问不存在的资源、提交无效数据),验证返回的错误响应是否包含标准的字段(错误码、错误消息、时间戳等),并且格式一致。
**Acceptance Scenarios**:
1. **Given** 用户请求一个不存在的资源, **When** 系统找不到该资源, **Then** 系统返回包含错误码(如 404)、中文错误描述、时间戳的标准 JSON 响应
2. **Given** 用户提交了格式错误的数据, **When** 系统验证失败, **Then** 系统返回包含错误码(如 400)、具体验证错误信息、时间戳的标准 JSON 响应
3. **Given** 系统内部发生未预期的错误, **When** 处理请求时出现异常, **Then** 系统返回包含错误码(500)、通用错误描述(不暴露内部细节)、时间戳的标准 JSON 响应
---
### User Story 2 - 系统稳定性保障(Panic 恢复) (Priority: P1)
当系统某个部分发生严重异常(panic)时,系统需要能够捕获并恢复,而不是整个服务崩溃,确保其他正在进行的请求不受影响,同时记录详细的错误信息供开发人员排查。
**Why this priority**: 这是系统可用性的关键保障。单个请求的错误不应该导致整个服务不可用,这直接关系到服务的稳定性和用户体验。
**Independent Test**: 可以创建一个测试端点故意触发 panic,验证系统是否能够捕获该 panic 并返回错误响应,同时其他端点仍然正常工作,且错误被记录到日志中。
**Acceptance Scenarios**:
1. **Given** 某个 API 处理程序内部发生 panic, **When** 请求到达该端点, **Then** 系统捕获 panic,返回 500 错误响应,服务继续运行,其他请求不受影响
2. **Given** 中间件处理过程中发生 panic, **When** 请求经过该中间件, **Then** 系统捕获 panic,返回错误响应,并记录完整的堆栈跟踪信息到日志
3. **Given** 多个并发请求中有一个触发 panic, **When** 系统处理这些请求, **Then** 只有触发 panic 的请求返回错误,其他请求正常完成
---
### User Story 3 - 业务错误分类处理 (Priority: P2)
运维人员和开发人员需要能够区分不同类型的错误(如客户端错误、服务端错误、业务逻辑错误),以便进行针对性的监控、告警和故障排查。
**Why this priority**: 这提升了系统的可维护性和可观测性,帮助团队更快地定位和解决问题,但不是系统能够运行的基础功能。
**Independent Test**: 可以触发不同类型的错误(验证失败、资源未找到、权限不足、系统内部错误),验证每种错误是否被正确分类,记录了适当的日志级别,并返回了相应的 HTTP 状态码。
**Acceptance Scenarios**:
1. **Given** 用户提交了业务上不允许的操作, **When** 系统验证业务规则, **Then** 系统返回 400 系列错误码,记录为 Warn 级别日志,包含业务错误码和描述
2. **Given** 系统依赖的外部服务不可用, **When** 尝试调用该服务, **Then** 系统返回 503 错误,记录为 Error 级别日志,包含重试提示
3. **Given** 数据库连接失败, **When** 执行数据库操作, **Then** 系统返回 500 错误,记录为 Error 级别日志,触发告警,不暴露敏感信息给客户端
---
### User Story 4 - 错误追踪和调试支持 (Priority: P3)
开发人员在排查问题时需要能够快速定位错误发生的位置和上下文,包括请求 ID、用户信息、错误堆栈等,以提高问题解决效率。
**Why this priority**: 这是运维和开发效率的提升,但不影响系统的核心功能和用户体验。
**Independent Test**: 可以触发一个错误,然后在日志中搜索该请求的 request_id,验证是否能找到完整的请求上下文(路径、方法、参数)和错误详情(堆栈、错误消息)。
**Acceptance Scenarios**:
1. **Given** 系统发生错误, **When** 查看日志, **Then** 日志包含请求 ID、请求路径、用户标识(如有)、错误类型、错误消息、时间戳
2. **Given** 需要追踪某个特定请求的完整流程, **When** 使用请求 ID 搜索日志, **Then** 可以找到该请求从接收到响应的所有日志条目
3. **Given** panic 发生, **When** 查看错误日志, **Then** 日志包含完整的 goroutine 堆栈跟踪,指明 panic 发生的确切位置
---
### Edge Cases
- 当错误处理程序本身发生错误或 panic 时,使用 defer + recover 保护机制,返回 HTTP 500 状态码和空响应体,避免无限循环或服务崩溃
- 当日志系统不可用时,系统采用静默失败策略,丢弃日志以确保请求响应不受影响
- 当响应已经部分发送给客户端后发生错误,采用静默失败策略:仅记录错误日志,不修改已发送的响应内容(避免破坏响应格式)
- 当错误信息包含敏感数据时,返回通用错误消息给客户端(如"内部服务器错误"),原始错误详情仅记录到受访问控制的日志系统
- 当并发请求量极高时,错误处理通过异步日志和最小化处理逻辑确保不成为性能瓶颈(目标延迟 < 1ms)
- 当客户端已断开连接时,错误处理仍会完成日志记录,但可跳过响应写入(Fiber 会自动处理已断开的连接)
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: 系统必须捕获所有路由处理程序和中间件中返回的错误,并统一处理;若响应已部分发送,则仅记录日志,不修改响应
- **FR-002**: 系统必须捕获所有 panic 异常,防止服务崩溃,并将 panic 转换为可控的错误响应
- **FR-003**: 系统必须为所有错误响应提供统一的 JSON 格式,包含错误码、错误消息、时间戳
- **FR-004**: 系统必须支持自定义错误类型,允许指定特定的 HTTP 状态码和错误消息
- **FR-005**: 系统必须记录所有错误到日志系统,包含请求上下文和错误详情;当日志系统不可用时采用静默失败策略
- **FR-006**: 系统必须区分客户端错误(4xx)和服务端错误(5xx),并返回相应的状态码
- **FR-007**: 系统必须在返回给客户端的错误响应中隐藏内部实现细节和敏感信息;所有错误返回通用错误消息,原始错误详情仅记录到受访问控制的日志系统
- **敏感信息明确定义**:以下信息类型严禁暴露给客户端
- 数据库错误详情 (SQL 语句、表名、字段名、约束冲突详情)
- 文件系统路径 (绝对路径、相对路径、文件名)
- 堆栈跟踪信息 (文件名、行号、函数调用链)
- 环境变量和配置值 (数据库连接串、API 密钥、服务地址)
- 内部服务名称和版本号
- 内存地址和对象引用
- 第三方服务的错误详情 (仅返回通用的"外部服务错误")
- **通用消息策略**:所有 5xx 错误统一返回"内部服务器错误"或"服务暂时不可用",4xx 错误返回业务相关的友好提示
- **FR-008**: 系统必须为每个错误关联请求 ID(通过响应 Header X-Request-ID 传递,不在响应体中包含),以便追踪和调试
- **FR-009**: 系统必须支持配置全局错误处理程序,允许自定义错误处理逻辑;ErrorHandler 必须使用 defer + recover 保护,当其自身发生 panic 时返回 HTTP 500 空响应体
- **FR-010**: panic 恢复后必须记录完整的堆栈跟踪信息到日志
### Technical Requirements (Constitution-Driven)
**Tech Stack Compliance**:
- [x] 使用 Fiber 框架的错误处理机制(ErrorHandler)
- [x] 使用 Fiber Recover 中间件处理 panic
- [x] 使用 Zap 记录错误日志,配置为静默失败模式(日志失败不影响请求处理)
- [x] 集成现有的 `pkg/response/` 统一响应格式
- [x] 使用 `pkg/errors/` 定义的错误码
**Architecture Requirements**:
- [x] 错误处理中间件应该全局注册,在所有其他中间件之前
- [x] Recover 中间件应该在错误处理中间件之后注册
- [x] 自定义错误类型应该在 `pkg/errors/` 包中定义
- [x] 错误响应格式应该通过 `pkg/response/` 包统一处理
- [x] 所有日志消息使用中文
- [x] 错误消息支持中文(面向用户的错误消息)
- [x] 客户端错误响应仅包含通用错误消息和错误码,不暴露原始错误详情(如数据库错误、文件路径、堆栈跟踪)
- [x] 日志系统访问需配置适当的权限控制,防止敏感信息泄露
**Go Idiomatic Design Requirements**:
- [x] 错误处理遵循 Go 的显式错误返回模式
- [x] 使用标准 error 接口,支持 errors.As 和 errors.Is
- [x] Panic 只用于真正的不可恢复错误,业务错误使用 error 返回
- [x] 错误信息简洁明确,便于调试
**API Design Requirements**:
- [x] 所有错误响应使用统一 JSON 格式
- [x] HTTP 状态码与错误类型一致(400 系列=客户端错误, 500 系列=服务端错误)
- [x] 错误响应包含业务错误码,便于前端识别
- [x] 错误消息对用户友好,同时在日志中记录技术细节
**Performance Requirements**:
- [x] 错误处理不应显著增加请求延迟(< 1ms)
- [x] 日志记录使用异步方式,避免阻塞请求;日志失败时静默处理,不阻塞响应
- [x] Panic 恢复不应导致内存泄漏
**Testing Requirements**:
- [x] 为错误处理中间件编写单元测试
- [x] 为 Recover 中间件编写单元测试,包括 panic 场景
- [x] 为自定义错误类型编写测试
- [x] 为错误处理程序编写集成测试,覆盖各种错误场景
- [x] 测试错误日志记录功能
- [x] 测试并发场景下的错误处理
### Key Entities *(include if feature involves data)*
- **Error**: 表示系统中的错误,包含错误码、错误消息、HTTP 状态码、原始错误(用于错误链)
- **ErrorResponse**: 表示返回给客户端的错误响应结构(JSON 响应体),包含 code(业务错误码)、message(错误描述)、timestamp(时间戳);request_id 通过响应 Header X-Request-ID 传递,不在响应体中
- **ErrorContext**: 表示错误发生时的上下文信息,包含请求路径、方法、参数、用户信息等
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: 系统能够捕获 100% 的 panic,确保服务不会因单个请求崩溃而停止
- **SC-002**: 所有 API 错误响应格式一致,包含必需字段(错误码、消息、时间戳)
- **SC-003**: 错误日志记录率达到 100%,所有错误都被记录到日志系统(日志失败时静默处理,不影响响应)
- **SC-004**: 客户端能够通过错误码准确识别错误类型,并采取相应的处理措施
- **SC-005**: 开发人员能够在 5 分钟内通过请求 ID 定位到错误的完整上下文
- **SC-006**: 错误处理增加的响应时间不超过 1ms
- **SC-007**: 错误响应不包含任何内部实现细节(数据库错误、文件路径、堆栈跟踪等)
- **SC-008**: 在高并发场景下(1000+ 并发请求),错误处理不会成为性能瓶颈

View File

@@ -0,0 +1,265 @@
# Tasks: Fiber 错误处理集成
**Feature**: 003-error-handling
**Generated**: 2025-11-14
**Status**: Ready for Implementation
## 概述
本文档按用户故事组织实施任务,每个用户故事代表一个独立可测试的增量功能。
**技术栈**: Go 1.25.4, Fiber v2, Zap, GORM, Asynq, PostgreSQL 14+, Redis 6.0+
**测试策略**: 单元测试 + 集成测试,目标覆盖率 90%+
## 实施策略
- **MVP 范围**: User Story 1 + User Story 2 (P1 优先级)
- **增量交付**: 每完成一个用户故事即可独立测试和部署
- **并行机会**: 标记 [P] 的任务可并行执行
---
## Phase 1: Setup (项目基础设施)
本阶段准备错误处理所需的基础代码结构。
### 任务列表
- [X] T001 审查现有错误处理代码 pkg/errors/errors.go 和 pkg/response/response.go
- [X] T002 审查现有中间件 internal/middleware/recover.go 实现
- [X] T003 确认 Request ID 中间件配置 (cmd/api/main.go 中的 requestid.New())
---
## Phase 2: Foundational (核心基础组件)
本阶段实现所有用户故事依赖的核心组件:错误码定义和错误上下文提取。
**阻塞关系**: 必须在所有用户故事实施前完成
### 任务列表
- [X] T004 创建 pkg/errors/codes.go 定义完整错误码枚举 (CodeSuccess, Code1001-1009, Code2001-2006)
- [X] T005 在 pkg/errors/codes.go 中实现错误消息映射表 errorMessages (中文消息)
- [X] T006 在 pkg/errors/codes.go 中实现 GetHTTPStatus() 函数 (错误码 -> HTTP 状态码映射)
- [X] T007 在 pkg/errors/codes.go 中实现 GetMessage() 函数 (获取错误码对应的消息)
- [X] T008 扩展 pkg/errors/errors.go 中的 AppError 结构体,添加 HTTPStatus 字段
- [X] T009 [P] 在 pkg/errors/errors.go 中实现 AppError.WithHTTPStatus() 方法
- [X] T010 [P] 在 pkg/errors/errors.go 中实现 AppError.Error() 方法 (实现 error 接口)
- [X] T011 [P] 在 pkg/errors/errors.go 中实现 AppError.Unwrap() 方法 (支持错误链)
- [X] T012 创建 pkg/errors/context.go 定义 ErrorContext 结构体
- [X] T013 在 pkg/errors/context.go 中实现 FromFiberContext() 函数 (从 Fiber Ctx 提取错误上下文)
- [X] T014 在 pkg/errors/context.go 中实现 ErrorContext.ToLogFields() 方法 (转换为 Zap 日志字段)
- [X] T015 在 pkg/constants/constants.go 中添加 Request ID 相关常量 (如需补充)
- [X] T016 [P] 为 pkg/errors/codes.go 编写单元测试 (测试错误码映射函数)
- [X] T017 [P] 为 pkg/errors/context.go 编写单元测试 (测试上下文提取逻辑)
**完成标志**: 错误码和错误上下文组件可被其他模块导入使用
---
## Phase 3: User Story 1 - 统一错误响应格式 (P1)
**目标**: 所有 API 错误返回统一的 JSON 格式,包含错误码、消息、时间戳
**独立测试标准**: 调用任意会产生错误的 API 端点,验证返回的 JSON 响应包含标准字段 (code, data, msg, timestamp),格式一致
### 任务列表
- [X] T018 [US1] 创建 pkg/errors/handler.go 实现 SafeErrorHandler() 函数 (返回 fiber.ErrorHandler)
- [X] T019 [US1] 在 pkg/errors/handler.go 中实现核心错误处理逻辑 handleError()
- [X] T020 [US1] 在 handleError() 中实现响应状态检查 (判断响应是否已发送)
- [X] T021 [US1] 在 handleError() 中实现错误类型分类 (*AppError, *fiber.Error, 其他 error)
- [X] T022 [US1] 在 handleError() 中实现错误消息脱敏逻辑 (5xx 返回通用消息)
- [X] T023 [US1] 在 handleError() 中集成 ErrorContext 提取和日志记录
- [X] T024 [US1] 在 handleError() 中实现统一 JSON 响应生成 (使用 fiber.Map)
- [X] T025 [US1] 在 handleError() 中设置响应 Header X-Request-ID
- [X] T026 [US1] 在 SafeErrorHandler() 中实现 defer + recover 保护机制 (防止 ErrorHandler 自身 panic)
- [X] T027 [US1] 更新 cmd/api/main.go 配置 Fiber ErrorHandler (使用 SafeErrorHandler)
- [X] T028 [US1] 为 pkg/errors/handler.go 编写单元测试 (测试不同错误类型的处理)
- [X] T029 [US1] 创建 tests/integration/error_handler_test.go 测试参数验证失败 -> 400 错误响应
- [X] T030 [US1] 在 tests/integration/error_handler_test.go 中测试资源未找到 -> 404 错误响应
- [X] T031 [US1] 在 tests/integration/error_handler_test.go 中测试认证失败 -> 401 错误响应
- [X] T032 [US1] 在 tests/integration/error_handler_test.go 中验证所有错误响应格式一致性
**完成标志**:
- 所有 API 错误响应使用统一 JSON 格式
- 集成测试覆盖常见错误场景 (400, 401, 404)
- 错误消息脱敏,不暴露内部细节
---
## Phase 4: User Story 2 - 系统稳定性保障(Panic 恢复) (P1)
**目标**: 捕获所有 panic 异常,防止服务崩溃,记录完整堆栈跟踪
**独立测试标准**: 创建测试端点触发 panic验证系统返回 500 错误响应,服务继续运行,其他端点正常工作,错误记录到日志
### 任务列表
- [X] T033 [US2] 审查现有 internal/middleware/recover.go 实现,确认是否需要调整
- [X] T034 [US2] 确保 recover 中间件在 Fiber 中间件链的正确位置注册 (ErrorHandler 之后)
- [X] T035 [US2] 在 recover 中间件中添加完整堆栈跟踪记录 (使用 runtime/debug.Stack())
- [X] T036 [US2] 在 recover 中间件中确保 panic 转换为可控的错误响应 (返回 AppError)
- [X] T037 [US2] 验证 recover 中间件与 ErrorHandler 的集成 (panic -> AppError -> ErrorHandler)
- [X] T038 [US2] 为 internal/middleware/recover.go 编写单元测试 (测试 panic 捕获)
- [X] T039 [US2] 在 tests/integration/error_handler_test.go 中创建测试端点触发 panic
- [X] T040 [US2] 在 tests/integration/error_handler_test.go 中测试 panic 恢复后服务继续运行
- [X] T041 [US2] 在 tests/integration/error_handler_test.go 中测试并发场景下的 panic 处理 (多个请求)
- [X] T042 [US2] 在 tests/integration/error_handler_test.go 中验证 panic 时的堆栈跟踪记录
- **验证堆栈跟踪完整性**:确保日志包含文件名、行号、函数名
- **验证堆栈深度**:检查是否包含从 panic 发生点到 recover 捕获点的完整调用链
- **验证格式可读性**:堆栈信息应便于开发人员快速定位问题
**完成标志**:
- 系统能捕获 100% 的 panic
- 单个请求 panic 不影响其他请求
- 日志包含完整的堆栈跟踪信息
---
## Phase 5: User Story 3 - 业务错误分类处理 (P2)
**目标**: 区分不同类型的错误 (客户端错误、服务端错误),记录适当的日志级别,返回相应的 HTTP 状态码
**独立测试标准**: 触发不同类型的错误 (验证失败、权限不足、数据库错误),验证错误分类正确,日志级别匹配 (客户端错误 Warn服务端错误 Error)HTTP 状态码正确
### 任务列表
- [X] T043 [P] [US3] 在 pkg/errors/codes.go 中实现 GetLogLevel() 函数 (错误码 -> 日志级别映射)
- [X] T044 [US3] 在 pkg/errors/handler.go 中集成 GetLogLevel(),根据错误类型记录不同日志级别
- [X] T045 [P] [US3] 在 tests/integration/error_handler_test.go 中测试参数验证失败 -> Warn 级别日志
- [X] T046 [P] [US3] 在 tests/integration/error_handler_test.go 中测试权限不足 -> Warn 级别日志
- [X] T047 [P] [US3] 在 tests/integration/error_handler_test.go 中测试数据库错误 -> Error 级别日志
- [X] T048 [US3] 在 tests/integration/error_handler_test.go 中验证敏感信息隐藏 (数据库错误不暴露 SQL)
- [X] T049 [US3] 在 tests/integration/error_handler_test.go 中测试限流错误 -> 429 响应
- [X] T050 [US3] 在 tests/integration/error_handler_test.go 中测试服务不可用 -> 503 响应
**完成标志**:
- 客户端错误 (1xxx) 记录为 Warn 级别,返回 4xx 状态码
- 服务端错误 (2xxx) 记录为 Error 级别,返回 5xx 状态码
- 敏感信息不暴露给客户端
---
## Phase 6: User Story 4 - 错误追踪和调试支持 (P3)
**目标**: 错误日志包含完整的请求上下文 (Request ID, 路径, 参数),便于快速定位和排查问题
**独立测试标准**: 触发一个错误,在日志中搜索 request_id验证能找到完整的请求上下文 (路径、方法、参数) 和错误详情
### 任务列表
- [X] T051 [P] [US4] 在 pkg/errors/context.go 中完善 ErrorContext 字段 (确保包含所有调试信息)
- [X] T052 [US4] 在 pkg/errors/handler.go 中确保错误日志包含所有 ErrorContext 字段
- [X] T053 [US4] 在 pkg/errors/handler.go 中添加请求参数记录 (Query 和 Body限制 50KB)
- [X] T054 [US4] 在 tests/integration/error_handler_test.go 中测试错误日志完整性 (包含 Request ID)
- [X] T055 [US4] 在 tests/integration/error_handler_test.go 中测试请求上下文记录 (路径、方法、参数)
- [X] T056 [US4] 在 tests/integration/error_handler_test.go 中测试 panic 堆栈跟踪记录 (指明 panic 位置)
- [X] T057 [US4] 在 tests/integration/error_handler_test.go 中测试使用 Request ID 追踪请求流程
**完成标志**:
- 所有错误日志包含 Request ID
- 日志包含完整的请求上下文 (路径、方法、参数)
- Panic 日志包含完整的堆栈跟踪
---
## Phase 7: Polish & Cross-Cutting Concerns
本阶段完善文档、性能优化和最终验证。
### 任务列表
- [X] T058 运行所有单元测试并验证覆盖率 > 90% (pkg/errors/ 包)
- [X] T059 运行所有集成测试并验证所有场景通过
- [X] T060 运行性能基准测试,验证错误处理延迟 < 1ms (P95)
- [X] T061 在高并发场景下测试错误处理 (1000+ 并发请求)
- [X] T062 [P] 创建 docs/003-error-handling/功能总结.md (功能概述、核心实现、技术要点)
- [X] T063 [P] 创建 docs/003-error-handling/使用指南.md (如何使用错误处理机制)
- [X] T064 [P] 创建 docs/003-error-handling/架构说明.md (错误处理架构设计,可选)
- [X] T065 更新 README.md 添加错误处理功能的简短描述 (2-3 句话)
- [X] T066 更新 CLAUDE.md 添加错误处理相关技术栈信息 (如需)
- [X] T067 代码审查:验证所有注释和日志消息使用中文
- [X] T068 代码审查:验证没有硬编码的魔术数字或字符串 (3+ 次出现必须定义为常量)
- [X] T069 运行 `go fmt``golangci-lint` 检查代码质量
- [X] T070 最终验证:所有 Success Criteria (SC-001 到 SC-008) 已满足
---
## 依赖关系图
```
Setup (T001-T003)
Foundational (T004-T017) ← 阻塞所有用户故事
├→ User Story 1 (T018-T032) [P1] ← MVP 核心
├→ User Story 2 (T033-T042) [P1] ← MVP 核心
├→ User Story 3 (T043-T050) [P2] ← 依赖 US1, US2
├→ User Story 4 (T051-T057) [P3] ← 依赖 US1, US2
Polish (T058-T070) ← 依赖所有用户故事完成
```
**关键路径**: Setup → Foundational → US1 → US2 → US3 → US4 → Polish
**并行机会**:
- US1 阶段: T028 单元测试可与 T029-T032 集成测试并行
- US2 阶段: T038 单元测试可与 T039-T042 集成测试并行
- US3 阶段: T045, T046, T047 测试任务可并行执行
- US4 阶段: T054-T057 测试任务可并行执行
- Polish 阶段: T062, T063, T064 文档编写可并行执行
---
## 任务统计
- **总任务数**: 70 个任务
- **Setup**: 3 个任务
- **Foundational**: 14 个任务
- **User Story 1 (P1)**: 15 个任务
- **User Story 2 (P1)**: 10 个任务
- **User Story 3 (P2)**: 8 个任务
- **User Story 4 (P3)**: 7 个任务
- **Polish**: 13 个任务
- **可并行任务**: 18 个任务 (标记 [P])
**MVP 范围**: T001-T042 (Setup + Foundational + US1 + US2) = 42 个任务
**预估时间**:
- MVP (US1 + US2): 2-3 天
- 完整功能 (US1-US4): 4-5 天
- 包含文档和优化: 5-6 天
---
## 实施建议
1. **优先完成 MVP**: 先实现 US1 和 US2 (P1 优先级),确保核心错误处理和 panic 恢复功能可用
2. **增量测试**: 每完成一个用户故事立即进行集成测试,确保功能正确
3. **并行执行**: 利用标记 [P] 的任务并行开发,提高效率
4. **代码审查**: 在进入下一个用户故事前,审查当前代码质量
5. **性能验证**: 在 Polish 阶段进行性能测试,确保错误处理延迟 < 1ms
---
## 成功标准验证
完成所有任务后,验证以下成功标准:
-**SC-001**: 系统能够捕获 100% 的 panic (US2)
-**SC-002**: 所有 API 错误响应格式一致 (US1)
-**SC-003**: 错误日志记录率 100% (US1, US4)
-**SC-004**: 客户端能通过错误码识别错误类型 (US1, US3)
-**SC-005**: 5 分钟内通过 Request ID 定位错误 (US4)
-**SC-006**: 错误处理延迟 < 1ms (所有 US)
-**SC-007**: 错误响应不包含敏感信息 (US1, US3)
-**SC-008**: 高并发下错误处理不成为瓶颈 (US2, Polish)
---
**文档版本**: 1.0
**最后更新**: 2025-11-14
**下一步**: 运行 `/speckit.implement` 开始执行任务