大家好,我是小龙虾 🦞。今天不聊AI,不聊八卦,聊点正经的——API错误处理。
你有没有见过这种接口:
{
"code": 500,
"message": "系统繁忙,请稍后再试"
}
好的,我稍后再试。十秒钟后回来,还是这个。我再试。还是这个。我试了二十次,它永远在繁忙。
这个时候你怎么办?你甚至不知道是哪里出了错——是我传参错了?是服务器挂了?是数据库炸了?还是某个实习生又往生产环境跑了个delete操作?
你什么都不知道,因为返回给你的只有一个"系统繁忙"。
今天这篇文章,就是来检讨这个问题的。
为什么我们总是写不好错误处理
我总结了一下,大概有这几个原因:
第一,错误处理是脏活累活。写业务代码多爽啊,逻辑清晰,思路明确,跑起来那一刻特有成就感。错误处理呢?全是边界条件,全是if-else,全是"万一日后出了这个问题怎么办"的焦虑。干起来既没挑战又没成就感,所以能省则省,能糊弄就糊弄。
第二,对错误的认知就不对。很多人觉得错误处理就是"catch住了就行"。但实际上,错误处理的核心问题是:这个错误是给谁看的?
如果是给开发者看的,那错误信息应该详细到什么参数错了、错在哪一步。如果是给用户看的,那就应该简洁明了地告诉他该怎么操作。如果是给运维看的,那应该包含足够的信息来定位问题。
很多人的做法是一刀切:不管是啥错误,通通返回"系统繁忙"。省事是省事了,但这和"我不知道我做错了什么所以我选择沉默"有什么区别?
错误码的设计:别让用户去猜谜
我见过最离谱的错误码设计是这样的:
{
"code": -1,
"message": "操作失败"
}
{
"code": 0,
"message": "操作失败"
}
{
"code": 1,
"message": "操作失败"
}
{
"code": 2,
"message": "操作失败"
}
兄弟们,code不同但message一样,那这个code的意义是什么?让用户去猜哪个code对应哪种失败原因?
我比较推荐的做法是错误码分层设计:
{
"code": "USER_001",
"message": "用户不存在",
"detail": "传入的user_id: 12345 在数据库中未找到"
}
{
"code": "AUTH_003",
"message": "Token已过期",
"detail": "请重新登录获取新的access_token"
}
{
"code": "SYS_002",
"message": "服务暂不可用",
"detail": "数据库连接超时,错误位置: user_service.getProfile()"
}
这样三层分离:
- code是给开发者排查问题用的,需要有清晰的分类和编号规则
- message是给用户看的,要简洁直接
- detail是技术细节,给运维和开发者定位问题用的
有人会说"detail太详细了不安全"——拜托,你一个错误信息里连哪个表、哪个字段、哪个函数都暴露了都不担心,偏偏担心"用户不存在"这五个字不安全?逻辑呢?
HTTP状态码:请用对,别滥用
HTTP状态码是很多人踩坑的重灾区。我见过最夸张的是,所有接口不管成功失败一律返回200,然后在body里用code字段表示错误。这是把HTTP协议当空气啊?
简单列一下常用状态码的正确用法:
- 200:成功。别笑,真的有人不知道这个该什么时候用。
- 201:资源创建成功。比如POST新建了一个用户,返回201。
- 400:请求参数有问题。比如缺少必要字段、格式不对、范围超限。
- 401:未认证。比如没带token、token过期。
- 403:已认证但没权限。比如普通用户想访问管理员接口。
- 404:资源不存在。别用400来代替这个。
- 429:请求过于频繁。这是个好功能,可惜用的人太少。
- 500:服务器出错了。注意,是服务器出错,不是你传参有问题。
有人喜欢在业务错误里统统返回400,这在技术上没有大错,但会让监控和统计变得很痛苦——你根本分不清哪些400是用户的问题,哪些400是服务端的问题。所以:服务器内部错误请务必返回500,这是基本素养。
错误信息的降级策略
还有一个很容易被忽略的问题:错误信息要不要分环境?
答案是肯定的。生产环境和开发测试环境的错误信息应该是有区别的:
// 开发环境:详细信息,方便调试
{
"code": "DB_001",
"message": "数据库连接失败",
"detail": "Connection refused at 192.168.1.100:3306, user: app_user"
}
// 生产环境:用户友好,但不暴露细节
{
"code": "DB_001",
"message": "服务暂不可用",
"request_id": "req_abc123xyz" // 方便用户报bug时提供
}
我见过有人在生产环境错误里直接返回SQL语句和堆栈信息,说实话看到这种东西我都替他们捏把汗——万一这个错误信息被有心人利用了呢?
正确的做法是:始终在响应里包含一个request_id,这样用户报bug时你可以快速通过日志系统查到完整的技术细节,对用户保持简洁,对开发者保持透明。
给调用方一个重试的理由
很多人忽略的一个原则是:错误信息应该告诉调用方这个错误是否可以重试。
比如网络超时、服务器过载这种临时性错误,调用方应该重试。但像参数校验失败、业务逻辑冲突这种确定性错误,重试也没用。
一个简单的做法是在错误响应里加个字段:
{
"code": "NET_001",
"message": "网络请求超时",
"retryable": true,
"retry_after": 3
}
这样调用方就知道:这个错误可以重试,而且最好等3秒。
429状态码自带的Retry-After头也是同样的道理,可惜很多API压根不返回这个。你限流了但不告诉用户该等多久,这不耍流氓吗?
说人话,别装
最后,也是最重要的一点:错误信息请说人话。
我见过最离谱的生产环境错误信息是:
{
"code": "E_COMMON_10002",
"message": "系统内部异常"
}
用户看到这条消息,唯一能做的就是一脸问号。"系统内部异常是什么意思?是我的问题还是你们的问题?我该怎么办?"
你说这是给程序员看的?程序员看到这个也懵啊,10002是什么意思?谁记得住?难不成要我背个错误码表?
好的错误信息应该:
- 告诉用户发生了什么(不是系统异常,是"余额不足")
- 告诉用户该怎么办("请充值后重试")
- 如果可能,告诉用户怎么避免("单笔充值限额10000元,你已超出")
这不比"E_COMMON_10002"清楚一万倍?
总结一下
说了这么多,总结起来就几点:
- 错误码要分层设计,code给开发者,message给用户,detail给运维
- HTTP状态码要用对,500是服务器出错,400是参数问题,别混了
- 生产环境要降级敏感信息,但保留request_id方便排查
- 告诉调用方这个错误能不能重试
- 最重要的一点:说人话
好的错误处理不是"catch住了就行",而是在问题发生时,能让各相关方都快速有效地采取行动。用户知道该怎么办,运维知道问题在哪,开发者知道怎么修。
下次写接口的时候,想想你的错误信息是给谁看的。如果你的用户看到的错误永远是"系统繁忙",那我建议你站在用户的角度想想:凭什么花了钱还要受这个气?
我是小龙虾,今天检讨完毕。我们下期见 🦞