Files
huang fb83c9a706 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
2025-11-15 12:17:44 +08:00

13 KiB

Quick Start: Fiber 错误处理集成

Feature: 003-error-handling
Date: 2025-11-14
Audience: 新开发者、集成工程师

概述

本文档提供 Fiber 错误处理集成的快速上手指南,帮助开发者快速理解和使用统一的错误处理机制。

5 分钟快速开始

1. 错误响应格式

所有 API 错误响应都使用统一格式:

{
  "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: 使用预定义错误码

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 自动处理)

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: 使用预定义错误常量

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:

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:

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']

进阶使用

自定义错误消息

// 使用预定义错误码 + 自定义消息
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))
    }
    
    // 更新逻辑...
}

错误链传递

// 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 错误:

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:

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:

grep "f1d8b767-dfb3-4588-9fa0-8a97e5337184" logs/app.log

日志会包含完整的错误详情:

{
  "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: 参数验证失败

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: 资源未找到

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: 数据库错误

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: 外部服务不可用

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. 使用预定义错误码: 保持错误码的一致性

    return errors.New(errors.CodeInvalidParam, "用户名不能为空")
    
  2. 包装底层错误: 保留错误链,便于调试

    return errors.Wrap(errors.CodeDatabaseError, "查询失败", err)
    
  3. 提供友好的错误消息: 使用中文,面向用户

    return errors.New(errors.CodeNotFound, "订单不存在")
    
  4. 区分客户端和服务端错误: 使用正确的错误码范围

    • 1000-1999: 客户端问题 (参数错误、权限不足等)
    • 2000-2999: 服务端问题 (数据库错误、服务不可用等)

避免做法

  1. 不要硬编码错误消息

    // ❌ 错误
    return c.Status(400).JSON(fiber.Map{"error": "参数错误"})
    
    // ✅ 正确
    return errors.New(errors.CodeInvalidParam, "参数错误")
    
  2. 不要暴露敏感信息

    // ❌ 错误: 暴露 SQL 语句
    return errors.New(errors.CodeDatabaseError, err.Error())
    
    // ✅ 正确: 使用通用消息
    return errors.Wrap(errors.CodeDatabaseError, "查询失败", err)
    
  3. 不要滥用 panic

    // ❌ 错误: 业务错误不应该 panic
    if user == nil {
        panic("user not found")
    }
    
    // ✅ 正确: 使用 error 返回
    if user == nil {
        return errors.New(errors.CodeNotFound, "用户不存在")
    }
    
  4. 不要忽略错误

    // ❌ 错误: 忽略错误
    user, _ := h.service.GetByID(id)
    
    // ✅ 正确: 处理错误
    user, err := h.service.GetByID(id)
    if err != nil {
        return errors.Wrap(errors.CodeDatabaseError, "获取用户失败", err)
    }
    

测试错误处理

单元测试示例

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 中添加常量定义和错误消息映射:

const (
    CodeMyNewError = 1010 // 客户端错误
)

var errorMessages = map[int]string{
    CodeMyNewError: "我的新错误",
}

Q: 服务端错误为什么只返回通用消息?

A: 出于安全考虑,避免泄露数据库结构、文件路径等敏感信息。完整错误详情会记录到日志,运维团队可以通过 Request ID 查看。

Q: Panic 会导致服务崩溃吗?

A: 不会。Recover 中间件会捕获所有 panic,转换为 500 错误响应,确保服务继续运行。

Q: 如何在日志中搜索特定用户的所有错误?

A: 日志包含 user_id 字段 (如果已认证),可以搜索:

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