542 lines
11 KiB
Markdown
542 lines
11 KiB
Markdown
# API 错误处理
|
||
|
||
## 错误响应设计
|
||
|
||
一致且信息丰富的错误响应对于 API 的可用性至关重要。
|
||
|
||
## 标准错误格式
|
||
|
||
### 基础错误响应
|
||
|
||
```json
|
||
{
|
||
"error": {
|
||
"code": "RESOURCE_NOT_FOUND",
|
||
"message": "未找到 ID 为 123 的用户",
|
||
"details": null
|
||
}
|
||
}
|
||
```
|
||
|
||
### RFC 7807 问题详情
|
||
|
||
标准化错误格式 (application/problem+json):
|
||
|
||
```http
|
||
HTTP/1.1 404 Not Found
|
||
Content-Type: application/problem+json
|
||
|
||
```
|
||
|
||
**字段说明:**
|
||
- `type` — 标识错误类型的 URI 引用
|
||
- `title` — 简短、人类可读的摘要
|
||
- `status` — HTTP 状态码
|
||
- `detail` — 针对本次错误的人类可读解释
|
||
- `instance` — 指向本次错误具体实例的 URI 引用
|
||
|
||
### 扩展错误响应
|
||
|
||
```json
|
||
{
|
||
"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)
|
||
|
||
客户端发送了无效数据。
|
||
|
||
```http
|
||
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)
|
||
|
||
缺少或提供了无效的身份认证凭据。
|
||
|
||
```http
|
||
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)
|
||
|
||
已通过身份认证但无权执行操作。
|
||
|
||
```http
|
||
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)
|
||
|
||
资源不存在。
|
||
|
||
```http
|
||
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)
|
||
|
||
请求与当前状态冲突。
|
||
|
||
```http
|
||
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)
|
||
|
||
客户端超出了速率限制。
|
||
|
||
```http
|
||
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)
|
||
|
||
意外的服务器错误。
|
||
|
||
```http
|
||
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)
|
||
|
||
服务暂时不可用。
|
||
|
||
```http
|
||
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 定义标准错误码:
|
||
|
||
```json
|
||
{
|
||
"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": "内部服务器错误"
|
||
}
|
||
}
|
||
```
|
||
|
||
## 验证错误详情
|
||
|
||
### 字段级验证
|
||
|
||
```json
|
||
{
|
||
"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
|
||
}
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 跨字段验证
|
||
|
||
```json
|
||
{
|
||
"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 以便调试:
|
||
|
||
```http
|
||
Response Headers:
|
||
X-Request-ID: req_abc123
|
||
|
||
Response Body:
|
||
{
|
||
"error": {
|
||
"code": "INTERNAL_SERVER_ERROR",
|
||
"message": "发生意外错误",
|
||
"request_id": "req_abc123"
|
||
}
|
||
}
|
||
```
|
||
|
||
客户端可在工单中引用请求 ID。
|
||
|
||
## 错误文档
|
||
|
||
记录每个端点所有可能的错误:
|
||
|
||
```yaml
|
||
/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 的用户
|
||
```
|
||
|
||
## 重试指导
|
||
|
||
帮助客户端判断是否应该重试:
|
||
|
||
```json
|
||
{
|
||
"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
|
||
|
||
## 多语言支持
|
||
|
||
支持多语言错误消息:
|
||
|
||
```http
|
||
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** — 导致无法调试
|
||
- **未记录的错误** — 客户端不知道会遇到什么
|
||
- **信息过多** — 暴露内部实现细节
|