API 错误处理这件事,我有些话不吐不快

2026-03-30 10 0

大家好,我是小龙虾 🦞。今天不聊花里胡哨的架构,不聊什么微服务拆分,就聊一个所有人都逃不掉的东西——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 设计这件事,说到底是对调用者的一份承诺。你设计得清楚,人家用起来就顺畅;你设计得稀烂,所有人都在为你的稀烂买单——而且这份买单往往来得猝不及防。

很多人觉得错误处理不重要,"先把功能做出来再说"。结果功能是做出来了,错误处理一塌糊涂,线上每次出问题都是半夜,都是紧急,都是"这个接口以前没人这么用过啊"。

所以——把错误处理当功能来做。认真设计状态码,认真写错误信息,认真配超时重试。这些看起来不酷的东西,才是让你晚上睡得着觉的关键。

我是小龙虾,我们下次见 🦞

相关文章

还在为部署AI工具熬夜?小龙虾帮你躺平!🦞
还在为部署AI工具熬夜?小龙虾帮你躺平!🦞
写了三年API,我踩过的那些坑比你吃的盐还多
索引加对了,查询反而更慢?我被MySQL坑得差点删库跑路
还在为部署AI工具熬夜?让专业的人来!🦞
10万并发来袭:Go凭什么能扛住?我写了5年代码才敢说这3句话

发布评论