可靠 API 的目标不是“永不失败”,而是让调用方知道发生了什么、能否重试,以及重复请求会不会产生重复业务。本文以创建订单为例设计一个清晰的契约。
一、先定义成功与失败的 HTTP 语义
| 场景 | 状态码 | 说明 |
|---|---|---|
| 读取成功 | 200 | 返回资源或集合 |
| 创建成功 | 201 | 返回新资源,可带 Location |
| 删除成功且无正文 | 204 | 响应体为空 |
| 请求格式错误 | 400 | JSON 等语法无法解析 |
| 未认证/无权限 | 401/403 | 身份缺失与权限不足分开 |
| 资源不存在 | 404 | 目标资源无法找到 |
| 业务冲突 | 409 | 重复创建或状态冲突 |
| 字段校验失败 | 422 | 语法正确但内容无效 |
二、统一错误响应格式
{
"error": {
"code": "VALIDATION_FAILED",
"message": "请求参数未通过校验",
"details": {
"amount": ["金额必须大于 0"]
},
"request_id": "req_01J..."
}
}
code 给程序判断,message 给人阅读,details 给字段级提示,request_id 用于把用户报告与服务端日志关联。不要把堆栈、SQL 或密钥返回给客户端。
三、为创建类请求加入幂等键
网络超时后,客户端无法确定订单是否已创建。如果直接再次 POST,可能产生两笔订单。解决方法是由客户端为一次业务操作生成稳定的幂等键:
POST /api/orders HTTP/1.1
Content-Type: application/json
Idempotency-Key: 3c7244fa-8c63-4ca4-a81a-bb16fe0b9c1e
{"product_id": 1001, "quantity": 1}
服务端应在数据库中为“调用方 + 幂等键”建立唯一约束,并保存请求摘要、处理状态和最终响应。相同键且请求内容相同,返回第一次的结果;相同键但内容不同,应返回 409。
CREATE UNIQUE INDEX uq_idempotency_client_key
ON idempotency_records (client_id, idempotency_key);
四、避免先查后写的竞态条件
“先 SELECT 是否存在,再 INSERT”在并发下仍可能重复。正确做法是让数据库唯一约束成为最终防线,在事务中捕获唯一键冲突,并读取已经保存的结果。
五、只重试合适的失败
- 网络中断、408、429 和部分 5xx:可在幂等保证下指数退避重试。
- 400、401、403、404、422:通常需要修改请求或权限,不应盲目重试。
- 每次重试必须保留同一个幂等键;新业务操作使用新键。
六、验收测试
- 用同一幂等键并发发送两次完全相同的创建请求。
- 确认数据库只有一条订单,两个响应指向同一资源。
- 保持幂等键不变但修改数量,确认返回 409。
- 模拟处理完成后连接中断,再次请求应取回原响应。
- 在日志中通过
request_id找到完整链路。
安全边界
幂等键不能代替认证、授权和输入校验。键需要长度限制与字符校验,记录应设置合理过期时间,并防止不同用户互相读取结果。