你有没有遇到过这样的API:返回200状态码,但body里藏着错误信息?或者500错误只返回一行”Internal Server Error”,完全不知道哪里出了问题?好的错误处理不是锦上添花,而是API设计的基础设施。这篇文章我会从状态码选择、错误响应格式、到具体实现,完整地讲清楚REST API错误处理的最佳实践。
状态码是API错误处理的第一层信息。很多开发者要么全部返回200,要么全部返回500,这两种极端都不对。
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 200 OK | 请求成功 | GET请求返回数据 |
| 201 Created | 资源创建成功 | POST请求创建新资源 |
| 204 No Content | 成功但无返回内容 | DELETE请求成功删除 |
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 400 Bad Request | 请求参数错误 | 缺少必填字段、格式不对 |
| 401 Unauthorized | 未认证 | 缺少Token或Token过期 |
| 403 Forbidden | 无权限 | 已认证但无权访问该资源 |
| 404 Not Found | 资源不存在 | 请求的资源ID无效 |
| 409 Conflict | 资源冲突 | 重复创建、版本冲突 |
| 422 Unprocessable Entity | 语义错误 | 参数格式正确但语义不对 |
| 429 Too Many Requests | 请求过于频繁 | 触发限流 |
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 500 Internal Server Error | 服务器内部错误 | 未预期的异常 |
| 502 Bad Gateway | 网关错误 | 上游服务不可用 |
| 503 Service Unavailable | 服务不可用 | 维护中或过载 |
核心原则:4xx是客户端的错,5xx是服务端的错。不要用4xx掩盖服务端bug,也不要用5xx表示客户端参数错误。
选对状态码只是第一步,错误响应的body同样重要。推荐使用以下格式:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数验证失败",
"details": [
{
"field": "email",
"message": "邮箱格式不正确",
"rejected_value": "not-an-email"
},
{
"field": "age",
"message": "年龄必须在18-120之间",
"rejected_value": 15
}
],
"request_id": "req_abc123def456",
"doc_url": "https://docs.example.com/errors/VALIDATION_ERROR"
}
}
这个格式包含了:
以FastAPI为例,展示如何实现完整的错误处理体系:
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, EmailStr, validator
from typing import Optional, List
import uuid
import time
app = FastAPI()
# 自定义异常类
class AppError(Exception):
def __init__(self, code: str, message: str, status_code: int = 400,
details: Optional[list] = None):
self.code = code
self.message = message
self.status_code = status_code
self.details = details or []
class NotFoundError(AppError):
def __init__(self, resource: str, resource_id: str):
super().__init__(
code="NOT_FOUND",
message=f"{resource}不存在",
status_code=404,
details=[{"field": "id", "message": f"未找到ID为{resource_id}的{resource}"}]
)
class ConflictError(AppError):
def __init__(self, message: str):
super().__init__(
code="CONFLICT",
message=message,
status_code=409
)
class RateLimitError(AppError):
def __init__(self, retry_after: int = 60):
super().__init__(
code="RATE_LIMIT_EXCEEDED",
message=f"请求过于频繁,请在{retry_after}秒后重试",
status_code=429
)
self.retry_after = retry_after
# 全局异常处理器
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4())[:12])
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": exc.message,
"details": exc.details,
"request_id": request_id,
"doc_url": f"https://docs.example.com/errors/{exc.code}"
}
},
headers={"X-Request-ID": request_id}
)
@app.exception_handler(Exception)
async def general_error_handler(request: Request, exc: Exception):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4())[:12])
# 生产环境不要暴露异常详情
import logging
logging.error(f"[{request_id}] 未处理异常: {exc}", exc_info=True)
return JSONResponse(
status_code=500,
content={
"error": {
"code": "INTERNAL_ERROR",
"message": "服务器内部错误,请稍后重试",
"details": [],
"request_id": request_id
}
},
headers={"X-Request-ID": request_id}
)
# 请求ID中间件
@app.middleware("http")
async def add_request_id(request: Request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4())[:12])
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
# 数据模型
class CreateUserRequest(BaseModel):
email: EmailStr
username: str
age: int
@validator("username")
def validate_username(cls, v):
if len(v) < 3 or len(v) > 20:
raise ValueError("用户名长度必须在3-20之间")
if not v.isalnum():
raise ValueError("用户名只能包含字母和数字")
return v
@validator("age")
def validate_age(cls, v):
if v < 18 or v > 120:
raise ValueError("年龄必须在18-120之间")
return v
# 路由示例
@app.post("/users")
async def create_user(data: CreateUserRequest):
# 检查用户名是否已存在
if await check_username_exists(data.username):
raise ConflictError(f"用户名 '{data.username}' 已被使用")
user = await save_user(data)
return {"id": user.id, "username": user.username}
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user = await find_user(user_id)
if not user:
raise NotFoundError("用户", str(user_id))
return user
错误码是客户端程序化处理错误的关键。设计时遵循以下原则:
格式: {DOMAIN}_{SPECIFIC_ERROR}
示例:
VALIDATION_INVALID_EMAIL
VALIDATION_REQUIRED_FIELD
AUTH_TOKEN_EXPIRED
AUTH_INVALID_CREDENTIALS
RESOURCE_NOT_FOUND
RESOURCE_ALREADY_EXISTS
RATE_LIMIT_EXCEEDED
PAYMENT_CARD_DECLINED
PAYMENT_INSUFFICIENT_BALANCE
INTERNAL_SERVICE_UNAVAILABLE
INTERNAL_DATABASE_ERROR
规则:
API版本升级时,错误响应也要考虑兼容性:
# 在响应头中包含API版本
@app.middleware("http")
async def api_version_middleware(request: Request, call_next):
response = await call_next(request)
response.headers["API-Version"] = "v2"
return response
# 错误响应中也可以包含版本信息
{
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数验证失败",
"api_version": "v2",
"deprecated_codes": ["OLD_ERROR_FORMAT"],
"details": [...]
}
}
当服务过载时,优雅的错误处理尤为重要:
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@app.get("/api/data")
@limiter.limit("100/minute")
async def get_data(request: Request):
# 正常业务逻辑
return {"data": "..."}
# 降级处理
@app.get("/api/heavy-computation")
async def heavy_computation(request: Request):
try:
result = await compute_expensive_result()
return {"result": result}
except TimeoutError:
# 返回降级数据而不是500错误
return JSONResponse(
status_code=200,
content={
"data": get_cached_result(),
"meta": {
"degraded": True,
"message": "数据为缓存版本,实时计算暂时不可用"
}
}
)
好的API应该为每个错误码提供详细的文档页面。客户端看到错误响应中的doc_url后,可以直接跳转查看解决方案:
# 错误码: VALIDATION_ERROR
## 描述
请求参数未通过验证。
## 常见原因
1. 缺少必填字段
2. 字段格式不正确
3. 字段值超出允许范围
## 解决方案
检查请求body中的每个字段,确保:
- 所有必填字段都已提供
- 字段类型正确(字符串、数字、布尔值)
- 字段值在允许范围内
## 示例
### 错误请求
```json
{"email": "invalid", "age": 15}
{
"error": {
"code": "VALIDATION_ERROR",
"details": [
{"field": "email", "message": "邮箱格式不正确"},
{"field": "age", "message": "年龄必须在18-120之间"}
]
}
}
```
REST API的错误处理不是简单的try-catch,而是一个完整的体系:
把这些做好,你的API就不再是”能用就行”的水平,而是真正对开发者友好的专业级API。