Files
2026-07-13 21:36:01 +08:00

11 KiB
Raw Permalink Blame History

API 错误处理

错误响应设计

一致且信息丰富的错误响应对于 API 的可用性至关重要。

标准错误格式

基础错误响应

{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "未找到 ID 为 123 的用户",
    "details": null
  }
}

RFC 7807 问题详情

标准化错误格式 (application/problem+json)

HTTP/1.1 404 Not Found
Content-Type: application/problem+json

{
  "type": "https://api.example.com/errors/resource-not-found",
  "title": "资源未找到",
  "status": 404,
  "detail": "ID 为 123 的用户不存在",
  "instance": "/users/123"
}

字段说明:

  • type — 标识错误类型的 URI 引用
  • title — 简短、人类可读的摘要
  • status — HTTP 状态码
  • detail — 针对本次错误的人类可读解释
  • instance — 指向本次错误具体实例的 URI 引用

扩展错误响应

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求验证失败",
    "details": [
      {
        "field": "email",
        "code": "INVALID_FORMAT",
        "message": "邮箱必须是有效的电子邮件地址"
      },
      {
        "field": "age",
        "code": "OUT_OF_RANGE",
        "message": "年龄必须在 18 到 120 之间"
      }
    ],
    "request_id": "req_123456",
    "timestamp": "2024-01-15T10:30:00Z",
    "documentation_url": "https://api.example.com/docs/errors#validation-error"
  }
}

错误类别

1. 验证错误(400 Bad Request

客户端发送了无效数据。

POST /users
Content-Type: application/json

{
  "name": "",
  "email": "invalid-email",
  "age": 15
}

Response: 400 Bad Request
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求验证失败",
    "details": [
      {
        "field": "name",
        "code": "REQUIRED",
        "message": "姓名为必填项"
      },
      {
        "field": "email",
        "code": "INVALID_FORMAT",
        "message": "邮箱必须是有效的电子邮件地址"
      },
      {
        "field": "age",
        "code": "OUT_OF_RANGE",
        "message": "年龄必须至少为 18",
        "constraints": {
          "min": 18,
          "max": 120
        }
      }
    ]
  }
}

2. 身份认证错误(401 Unauthorized

缺少或提供了无效的身份认证凭据。

GET /users/123
Authorization: Bearer invalid_token

Response: 401 Unauthorized
WWW-Authenticate: Bearer realm="api", error="invalid_token"

{
  "error": {
    "code": "INVALID_TOKEN",
    "message": "访问令牌无效或已过期",
    "details": {
      "reason": "token_expired",
      "expired_at": "2024-01-15T10:00:00Z"
    }
  }
}

常见认证错误码:

  • MISSING_TOKEN — 未提供认证令牌
  • INVALID_TOKEN — 令牌格式错误或无效
  • EXPIRED_TOKEN — 令牌已过期
  • REVOKED_TOKEN — 令牌已被撤销

3. 授权错误(403 Forbidden

已通过身份认证但无权执行操作。

DELETE /users/123
Authorization: Bearer valid_token

Response: 403 Forbidden
{
  "error": {
    "code": "INSUFFICIENT_PERMISSIONS",
    "message": "您没有权限删除此用户",
    "details": {
      "required_permission": "users:delete",
      "your_permissions": ["users:read", "users:update"]
    }
  }
}

4. 未找到错误(404 Not Found

资源不存在。

GET /users/99999

Response: 404 Not Found
{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "未找到 ID 为 99999 的用户",
    "details": {
      "resource_type": "User",
      "resource_id": "99999"
    }
  }
}

5. 冲突错误(409 Conflict

请求与当前状态冲突。

POST /users
Content-Type: application/json

{
  "email": "existing@example.com",
  "name": "John Doe"
}

Response: 409 Conflict
{
  "error": {
    "code": "RESOURCE_ALREADY_EXISTS",
    "message": "邮箱 'existing@example.com' 对应的用户已存在",
    "details": {
      "field": "email",
      "value": "existing@example.com",
      "existing_resource": "/users/123"
    }
  }
}

6. 速率限制(429 Too Many Requests

客户端超出了速率限制。

GET /users

Response: 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705320000

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "您已超出速率限制",
    "details": {
      "limit": 100,
      "window": "1 hour",
      "retry_after": 60,
      "reset_at": "2024-01-15T11:00:00Z"
    }
  }
}

7. 服务器错误(500 Internal Server Error

意外的服务器错误。

GET /users/123

Response: 500 Internal Server Error
{
  "error": {
    "code": "INTERNAL_SERVER_ERROR",
    "message": "发生意外错误,请稍后重试。",
    "request_id": "req_123456",
    "timestamp": "2024-01-15T10:30:00Z"
  }
}

切勿暴露:

  • 堆栈跟踪
  • 数据库错误
  • 内部路径
  • 敏感配置

8. 服务不可用(503 Service Unavailable

服务暂时不可用。

GET /users

Response: 503 Service Unavailable
Retry-After: 300

{
  "error": {
    "code": "SERVICE_UNAVAILABLE",
    "message": "服务因维护暂时不可用",
    "details": {
      "retry_after": 300,
      "maintenance_end": "2024-01-15T12:00:00Z"
    }
  }
}

错误码目录

为您的 API 定义标准错误码:

{
  "VALIDATION_ERROR": {
    "status": 400,
    "description": "请求验证失败",
    "subcodes": {
      "REQUIRED": "缺少必填字段",
      "INVALID_FORMAT": "字段格式无效",
      "OUT_OF_RANGE": "值超出允许范围",
      "INVALID_ENUM": "值不在允许集合中"
    }
  },
  "AUTHENTICATION_ERROR": {
    "status": 401,
    "description": "身份认证失败",
    "subcodes": {
      "MISSING_TOKEN": "未提供认证令牌",
      "INVALID_TOKEN": "令牌无效",
      "EXPIRED_TOKEN": "令牌已过期"
    }
  },
  "AUTHORIZATION_ERROR": {
    "status": 403,
    "description": "权限不足",
    "subcodes": {
      "INSUFFICIENT_PERMISSIONS": "缺少所需权限",
      "RESOURCE_FORBIDDEN": "禁止访问资源"
    }
  },
  "RESOURCE_NOT_FOUND": {
    "status": 404,
    "description": "资源未找到"
  },
  "CONFLICT_ERROR": {
    "status": 409,
    "description": "请求与当前状态冲突",
    "subcodes": {
      "RESOURCE_ALREADY_EXISTS": "资源已存在",
      "CONCURRENT_MODIFICATION": "资源已被其他请求修改"
    }
  },
  "RATE_LIMIT_EXCEEDED": {
    "status": 429,
    "description": "超出速率限制"
  },
  "INTERNAL_SERVER_ERROR": {
    "status": 500,
    "description": "内部服务器错误"
  }
}

验证错误详情

字段级验证

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求验证失败",
    "details": [
      {
        "field": "credit_card.number",
        "code": "INVALID_FORMAT",
        "message": "信用卡号必须为 16 位数字",
        "value_provided": "1234",
        "constraints": {
          "pattern": "^[0-9]{16}$"
        }
      },
      {
        "field": "items[0].quantity",
        "code": "OUT_OF_RANGE",
        "message": "数量必须至少为 1",
        "value_provided": 0,
        "constraints": {
          "min": 1,
          "max": 1000
        }
      }
    ]
  }
}

跨字段验证

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求验证失败",
    "details": [
      {
        "fields": ["start_date", "end_date"],
        "code": "INVALID_RANGE",
        "message": "结束日期必须在开始日期之后",
        "values_provided": {
          "start_date": "2024-01-20",
          "end_date": "2024-01-15"
        }
      }
    ]
  }
}

请求 ID 追踪

始终包含请求 ID 以便调试:

Response Headers:
X-Request-ID: req_abc123

Response Body:
{
  "error": {
    "code": "INTERNAL_SERVER_ERROR",
    "message": "发生意外错误",
    "request_id": "req_abc123"
  }
}

客户端可在工单中引用请求 ID。

错误文档

记录每个端点所有可能的错误:

/users/{id}:
  get:
    responses:
      '200':
        description: 成功
      '401':
        description: 身份认证失败
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Error'
            examples:
              missing_token:
                value:
                  error:
                    code: MISSING_TOKEN
                    message: 未提供认证令牌
              invalid_token:
                value:
                  error:
                    code: INVALID_TOKEN
                    message: 令牌无效或已过期
      '404':
        description: 用户未找到
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Error'
            examples:
              not_found:
                value:
                  error:
                    code: RESOURCE_NOT_FOUND
                    message: 未找到 ID 为 123 的用户

重试指导

帮助客户端判断是否应该重试:

{
  "error": {
    "code": "SERVICE_UNAVAILABLE",
    "message": "服务暂时不可用",
    "retry": {
      "retryable": true,
      "retry_after": 60,
      "max_retries": 3,
      "backoff": "exponential"
    }
  }
}

可重试的错误

  • 408 Request Timeout
  • 429 Too Many Requests(带 Retry-After
  • 500 Internal Server Error(部分情况)
  • 502 Bad Gateway
  • 503 Service Unavailable
  • 504 Gateway Timeout

不可重试的错误

  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Entity

多语言支持

支持多语言错误消息:

GET /users/invalid
Accept-Language: es

Response: 404 Not Found
Content-Language: es
{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "Usuario con ID 'invalid' no encontrado"
  }
}

始终包含 code 字段,以便客户端实现自己的翻译。

最佳实践

  1. 使用标准 HTTP 状态码 — 不要在出错时返回 200
  2. 包含机器可读的错误码 — 供客户端逻辑使用的错误码
  3. 提供人类可读的消息 — 清晰的解释说明
  4. 具体但安全 — 不要暴露敏感信息
  5. 包含请求 ID — 用于追踪和调试
  6. 记录所有错误 — 每个端点每种可能的错误
  7. 保持一致性 — 所有端点使用相同格式
  8. 帮助客户端重试 — 指明错误是否可重试
  9. 尽早验证 — 立即返回验证错误
  10. 在服务端记录错误 — 追踪错误以进行监控

反模式

避免以下错误做法:

  • 通用的错误消息 — 没有详细信息的"发生错误"
  • 暴露堆栈跟踪 — 安全风险
  • 不一致的错误格式 — 不同端点使用不同结构
  • 缺少错误码 — 只有人类可读的消息
  • 错误的状态码 — 在响应体中返回错误时状态码为 200
  • 没有请求 ID — 导致无法调试
  • 未记录的错误 — 客户端不知道会遇到什么
  • 信息过多 — 暴露内部实现细节