做后端开发这些年,我见过最离谱的错误处理是这样的:所有错误一律返回200,然后在body里写{"code": 500, "message": "服务器打了个喷嚏"}。
我问他为什么,他说这样前端好处理。
我当时就想把键盘糊他脸上。不是,前端好处理了,那调试的时候后端日志里写什么?"用户说他看到服务器打了个喷嚏,请问这是哪位神仙在做法?"
HTTP状态码不是装饰品,它是API的门脸,是给调用者看的第一张名片。可惜99%的项目把它当成了摆设。
状态码的真正价值:语义先行
HTTP状态码是一套精密的语义系统,不是简单的"成功/失败"二元论。很多人以为200就是成功,400就是失败,500就是服务器炸了。实际上这套体系远比这复杂。
2xx不只是200。201 Created,资源创建成功;202 Accepted,异步任务接受但还没完成;204 No Content,成功但没有返回体——这个在DELETE操作里特别有用,你删就删了,别废话。但大部分国内团队的状态码字典就三个:200、400、500。能用500解决的事,绝不费脑子想别的。
400家族才是重灾区。401和403的区别是什么?前者是没认证,后者是认证了但没权限。很多人分不清,反正都是"不行"。404和405呢?前者是资源不存在,后者是方法不允许。400和422呢?前者是通用错误,后者是语义校验失败——你的参数格式对了,但值不对,比如email字段填了"哈哈哈哈"。
最被滥用的是404。我见过一个接口,"用户不存在"返回404,"订单不存在"返回404,"商品下架了"还是返回404。前端拿到404,能知道是哪的问题?它只能知道"有东西没了",没了什么,不知道。
错误响应体:给机器看,也给人看
一个合格的错误响应体应该长这样:
{
"error": {
"code": "INVALID_PARAMETER",
"message": "邮箱格式不正确",
"field": "email",
"request_id": "req_abc123"
}
}
code是给程序看的机器码,用于前端做分支判断;message是给人看的文字,用于展示给用户或者给后端调试;request_id是这次请求的唯一标识,关联到日志系统,出了问题直接grep。
但很多项目的错误体是这样的:
{
"msg": "参数错误",
"code": -1
}
参数错误?哪个参数?什么错误?msg是给用户看的还是给日志看的?没有request_id怎么链路追踪?code=-1是什么意思,你们团队是要做隐藏任务吗?
还有人喜欢在错误信息里写"操作失败,请稍后重试"。这句话翻译一下就是"我不知道哪里错了,你猜猜"。如果重试有用,我还要后端工程师干什么?
异常分层:别让所有错误都变成try-catch里的Exception
我见过最恐怖的项目是这样的:所有业务异常一律throws Exception,然后上层统一catch (Exception e)打印日志,返回"系统繁忙"。
try {
// 查用户
// 查订单
// 查库存
// 下单
} catch (Exception e) {
log.error("系统错误", e);
return "系统繁忙,请稍后重试";
}
这种代码,运行时出了任何问题都是同一套输出。用户输入了错误的邮箱,它告诉你"系统繁忙请稍后重试";数据库连接池满了,它还是"系统繁忙请稍后重试";磁盘满了,它依然"系统繁忙请稍后重试"。用户以为系统在休息,其实是后端在裸奔。
正确的做法是分层捕获:
try {
// 业务逻辑
} catch (ValidationException e) {
// 参数校验失败,400
return error(400, "INVALID_PARAMETER", e.getMessage());
} catch (ResourceNotFoundException e) {
// 资源不存在,404
return error(404, "NOT_FOUND", e.getMessage());
} catch (InsufficientStockException e) {
// 库存不足,409
return error(409, "INSUFFICIENT_STOCK", e.getMessage());
} catch (Exception e) {
// 未知错误,500,但日志要打完整
log.error("Unexpected error", e);
return error(500, "INTERNAL_ERROR", "系统异常");
}
每一种异常对应一种状态码,每一种状态码对应一种用户可见的错误信息。不是所有的错都要用户知道细节,但日志里必须记清楚。
别忘了错误也分优先级
有些错误是用户造成的(4xx),有些是服务器造成的(5xx),有些是网络造成的(超时)。对于5xx,原则很简单:能自动恢复的重试,不能恢复的上报。但很多项目5xx一出,前端就开始无限重试,一秒钟十次,直到服务器彻底躺平。这不叫容错,这叫DDoS。
超时错误也要分级。连接超时和读取超时是两回事。前者是TCP握手没完成,后者是服务器响应太慢但已经开始返回数据了。读超时了你重试没问题,但写操作重试前要确认请求是否真的没到达服务器,否则可能出现重复下单。幂等性设计不是玄学,是基本功。
说到底,错误处理是一种承诺
你的API对外承诺了什么,错误时就要还什么。用户说"你这个接口能在用户不存在时返回一个友好的提示",你就不能返回一个500然后让前端自己猜用户去哪了。
很多后端工程师有个毛病:重开发轻错误。功能上线跑通了就算完成任务,错误处理留到线上出了事故再补。运气好的,出了事故能热修复;运气不好的,线上报警半夜响起,你对着"系统繁忙请稍后重试"想哭。
错误处理做得好不好,就看线上出了事故你要花多少时间定位问题。如果日志里写着"参数错误"而你需要化三天时间才找到是哪个参数、为什么错,那你欠下的技术债,早晚连本带利还回去。
下次写API的时候,在return 200之前先停一下,想一想:如果这里出错了,我打算告诉调用者什么?
如果你只能说出"系统繁忙"四个字,那这篇文章就是写给你的。