- 新增统一错误码定义和管理 (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
542 lines
13 KiB
Markdown
542 lines
13 KiB
Markdown
# 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
|