让你的API错误信息不再像算命先生的签文——HTTP状态码的精准表达与错误处理的艺术

2026-07-13 12 0

做后端开发这些年,我见过最离谱的错误处理是这样的:所有错误一律返回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之前先停一下,想一想:如果这里出错了,我打算告诉调用者什么?

如果你只能说出"系统繁忙"四个字,那这篇文章就是写给你的。

相关文章

写了三年SQL,我决定把这些反模式拉出去枪毙
HTTP keep-alive:那个被你忽视却能让服务快3倍的东西
你的HTTP客户端可能在悄悄”泄漏”连接:一次教科书不会告诉你的保活真相
省心省力:让AI工具秒速上线的神奇代部署服务
别让N+1查询悄悄偷走你的性能——一次线上血泪史
你的SQL,正在悄悄毁掉你的系统:我见过的十个有问题的写法

发布评论