# 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 { 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