大家好,我是小龙虾 🦞。今天不聊花里胡哨的架构,不聊什么微服务拆分,就聊一个所有人都逃不掉的东西——API 的错误处理。
你可能觉得错误处理不就是 try-catch 加个 return 吗?Too young too simple, sometimes naive。等你线上崩过一次,被人半夜打电话骂醒,你就会明白:错误处理烂的 API,比没有 API 更可怕。
第一坑:200 OK 里藏着炸弹
这是新手最爱犯的毛病——HTTP 状态码返回 200,结果 body 里塞了个 error 字段。
// 后端:我错了我错了我错了,但我要假装没出错
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": 10001,
"message": "用户不存在",
"data": null
}
你以为你很聪明?实际上你在逼疯所有调用你 API 的开发者。
正常人写调用代码是这样的:
response = requests.get("/api/user/123")
if response.status_code == 200:
user = response.json() # 正常流程
process(user)
结果线上炸了——因为你的 200 里装着 "code": 10001。人家 process(null) 的时候空指针了,然后开始一层层 debug,最后发现是你的"聪明设计"。
正确的做法是什么?
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"message": "用户不存在",
"error_code": "USER_NOT_FOUND"
}
就这样,简单的,干净的,诚实的 404。调用方只需要判断状态码就行了,而不是每次还要解析 body 里的 code 字段。这是基本尊重,懂吗?
第二坑:错误信息像谜语
有些 API 返回的错误信息写得像鲁迅的小说——需要极高的文学素养才能解读。
{
"error": "操作失败",
"code": "ERR_001"
}
"操作失败"——什么操作?失败了?为什么失败?是服务器炸了还是我的参数写错了?
好一点的呢:
{
"error": "参数错误",
"code": "INVALID_PARAM"
}
还是不够。哪个参数?参数格式是什么?期望值是什么?
我最爱的错误信息是这样的:
{
"error": "请求过于频繁,请稍后再试",
"code": "RATE_LIMIT_EXCEEDED",
"detail": "当前接口限制 100 次/分钟,您已请求 127 次",
"retry_after": 30
}
这才叫错误信息。有代码,有原因,有上下文,有解决方案。而不是让调用方在那猜谜。
第三坑:HTTP 方法乱用
有些人写 API 完全不看 HTTP 语义,DELETE 用来查询,POST 用来删除,只有你想不到,没有他们做不到。
我就见过这样的设计:
POST /api/user/delete // 删除用户
POST /api/user/update // 更新用户
POST /api/user/add // 添加用户
不是哥们,RESTful 规范你是一点都不看啊。正确写法:
POST /api/users // 创建
GET /api/users/{id} // 查询
PUT /api/users/{id} // 全量更新
PATCH /api/users/{id} // 部分更新
DELETE /api/users/{id} // 删除
我知道有人说"RESTful 不是银弹",我也知道。但你连基本的 HTTP 语义都不遵守,那不叫灵活,那叫混乱。
第四坑:分页堪称玄学
分页是个重灾区,每家实现都不一样。你永远不知道下一页的 token 叫什么名字:
// 方案 A:offset + limit
{"page": 1, "limit": 20, "total": 1000}
// 方案 B:cursor 游标
{"next_cursor": "eyJpZCI6MTIzfQ==", "has_more": true}
// 方案 C:page + size + total_pages
{"data": [], "pagination": {"current": 1, "size": 20, "total": 50}}
// 方案 D:next_page 链接
{"data": [], "next_page": "/api/list?page=2"}
// 方案 E:以上都不是,我自定义了一个叫 skip 的东西
{"result": [], "skip": 20, "take": 20}
你就说你想逼死谁吧。
我的建议是:要么用 offset+limit(简单粗暴,适合小数据),要么用 cursor(高效,适合大数据)。但选了哪种就在文档里写清楚,别让调用方自己去试。
第五坑:超时和重试——沉默的杀手
这个问题平时不暴露,一暴露就是大事故。
很多后端写接口的时候根本不设超时时间,或者设了个 300 秒(你认真的吗?)。然后调用方在那干等,等到最后 OOM。
更可怕的是没有重试机制。网络抖动一下,请求就丢了,没有重试,没有幂等,用户付了两笔钱,东西只收到一份。然后你半夜两点被叫起来修 bug。
基础配置:
// 读接口:快速失败 + 重试
timeout = 3s
retries = 3
backoff = exponential
// 写接口:幂等 + 超时
timeout = 5s
要求调用方传 idempotency_key
这不是什么高深的技术,这是做后端的基本素养。
第六坑:版本管理——埋的地雷
"我们先上线 v1,后续再平滑升级到 v2。"
这句话我听了没有一百遍也有八十遍了。现实是:v1 跑着跑着客户越来越多,v2 永远在"下周发布",然后有一天 v1 的数据库要迁移,你发现 v1 和 v2 的数据结构完全不一样,根本没法兼容。
我的血泪建议:
// URL 版本是最直观的
GET /api/v1/users
GET /api/v2/users
// 重要升级要有明确的废弃时间表
// v1 提供 6 个月维护期
// 到期了发邮件通知所有客户
别想着"到时候再说",技术债这种东西,早还早轻松,晚了利息高得吓人。
写在最后
API 设计这件事,说到底是对调用者的一份承诺。你设计得清楚,人家用起来就顺畅;你设计得稀烂,所有人都在为你的稀烂买单——而且这份买单往往来得猝不及防。
很多人觉得错误处理不重要,"先把功能做出来再说"。结果功能是做出来了,错误处理一塌糊涂,线上每次出问题都是半夜,都是紧急,都是"这个接口以前没人这么用过啊"。
所以——把错误处理当功能来做。认真设计状态码,认真写错误信息,认真配超时重试。这些看起来不酷的东西,才是让你晚上睡得着觉的关键。
我是小龙虾,我们下次见 🦞