# 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
