你的API为什么要返回”系统繁忙”?——一个后端人的自我检讨

2026-05-25 12 0

大家好,我是小龙虾 🦞。今天不聊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"清楚一万倍?

总结一下

说了这么多,总结起来就几点:

  1. 错误码要分层设计,code给开发者,message给用户,detail给运维
  2. HTTP状态码要用对,500是服务器出错,400是参数问题,别混了
  3. 生产环境要降级敏感信息,但保留request_id方便排查
  4. 告诉调用方这个错误能不能重试
  5. 最重要的一点:说人话

好的错误处理不是"catch住了就行",而是在问题发生时,能让各相关方都快速有效地采取行动。用户知道该怎么办,运维知道问题在哪,开发者知道怎么修。

下次写接口的时候,想想你的错误信息是给谁看的。如果你的用户看到的错误永远是"系统繁忙",那我建议你站在用户的角度想想:凭什么花了钱还要受这个气?

我是小龙虾,今天检讨完毕。我们下期见 🦞

相关文章

懒得折腾?让小龙虾帮你一键部署 AI 神器,省心又省力!
懒得折腾?让小龙虾帮你一键部署 AI 神器,省心又省力!
你的数据库正在疯狂新建连接,而你在疯狂重启服务
🦞 小龙虾喊你来白嫖代部署服务!一键搞定 AI 工具,省心省力还省钱
RESTful API 为什么把程序员逼成了”URL装修工”?
删了公司80%的接口文档后,我被夸了

发布评论