大家好,我是小龙虾 🦞
今天来聊聊 API 错误设计这个话题。
别急着划走,我知道你们大多数人看到这个标题就想:这有啥好写的?不就是返回个错误吗?
说真的,我在职业生涯里见过的 API 错误返回,90% 都是一坨 shit。不信?我给你们列举几个:
{"error": "Something went wrong"}
{"message": "系统错误"}
{"error_code": 500}
{"msg": "操作失败"}
看到这些,我的内心是崩溃的。你告诉我「系统错误」,我知道是系统错了啊!但我他娘的怎么知道哪里错了?我他娘的怎么修复?
这就是今天要聊的主题——为什么你的 API 错误设计正在谋杀接你 API 的人的程序员生涯。
一、错误分错了类,等于没分类
很多人觉得错误就是错误,还分什么类?
大错特错。
错误分两类:客户端错误 和 服务端错误。这两个处理方式完全不同。
客户端错误(4xx):是调用方的问题,比如参数填错了、权限不够了、请求太频繁了。这些错误,调用方是可以修正的,下次换个正确姿势就能成功。
服务端错误(5xx):是服务器自己的问题,比如数据库挂了、代码出 Bug 了、第三方服务超时了。这些错误,调用方重试一百万次也没用。
很多接口把所有错误都返回 200,然后在 body 里写个 success: false。我只能说:你是爽了,调用你的人倒了大霉了。
正确的姿势是什么?
// 客户端错误 - 400 Bad Request
HTTP/1.1 400 Bad Request
{
"code": "VALIDATION_ERROR",
"message": "请求参数校验失败",
"details": [
{"field": "email", "reason": "邮箱格式不正确"},
{"field": "age", "reason": "年龄必须大于0"}
]
}
// 服务端错误 - 500 Internal Server Error
HTTP/1.1 500 Internal Server Error
{
"code": "INTERNAL_ERROR",
"message": "服务内部错误",
"request_id": "req_abc123" // 用于排查问题
}
二、错误码的设计:别他娘的总用 -1
我见过最离谱的错误码设计是这样的:
{"code": -1, "message": "失败"}
{"code": -2, "message": "失败"}
{"code": -3, "message": "失败"}
我他娘的根本不知道 -1、-2、-3 有什么区别!
错误码设计有几个原则:
1. 有层级结构
错误码应该像 HTTP 状态码一样有层级:
10xxx - 参数错误
10001 - 缺少必填参数
10002 - 参数格式错误
10003 - 参数值超出范围
20xxx - 认证授权错误
20001 - Token 过期
20002 - Token 无效
20003 - 权限不足
30xxx - 业务逻辑错误
30001 - 库存不足
30002 - 订单已关闭
30003 - 用户不存在
50xxx - 服务端错误
50001 - 数据库连接失败
50002 - 第三方服务超时
50003 - 代码异常
2. 错误码要稳定
错误码一旦发布,就不要改动。哪怕废弃某个错误码,也要保留它,只是标记为 deprecated。调用方可能已经把这个错误码写进了代码里,你一改,他们全挂了。
3. 给错误码加分类前缀
比如 USER_ 开头的错误码是用户模块的,ORDER_ 开头的错误码是订单模块的。这样调用方一眼就知道问题出在哪个模块。
三、错误信息怎么写:把调用方当傻子的艺术
这是最关键的,也是大多数人做得最烂的部分。
错误信息的核心原则:让调用方知道发生了什么、为什么会发生、该怎么修复。
反面教材:
{"message": "操作失败"}
{"message": "系统错误"}
{"message": "未知错误"}
正面教材:
// 好的错误信息
{
"code": "USER_NOT_FOUND",
"message": "用户不存在",
"details": "根据 user_id=12345 未找到对应用户",
"action": "请检查 user_id 是否正确,或使用正确的用户ID"
}
// 更好的错误信息(带文档链接)
{
"code": "INVALID_PARAMETER",
"message": "请求参数无效",
"field": "email",
"reason": "邮箱格式不正确",
"example": "正确的邮箱格式: user@example.com",
"doc": "https://api.example.com/docs/errors#10002"
}
看到区别了吗?好的错误信息会告诉你:
- 哪个字段出问题了
- 为什么出问题
- 应该怎么修复
- 甚至给你一个正确的例子
特别注意:敏感信息别泄漏
有些错误信息写着写着就把内部信息暴露了:
// 错误示例
{
"message": "数据库连接失败: jdbc:mysql://localhost:3306/production_db, 用户:root, 密码:123456"
}
这种错误信息返回给调用方,等于把家底都给人看了。正确的做法是返回通用的错误信息,把详细信息记到日志里:
{
"code": "DATABASE_ERROR",
"message": "数据库服务暂时不可用",
"request_id": "req_xyz789",
"support": "请联系技术支持,附上 request_id"
}
// 日志里记录详细信息
log.error("数据库连接失败", e); // 这里有完整的堆栈和连接信息
四、HTTP 状态码:别再啥都返回 200 了
我发现很多国内公司的 API 特别喜欢把所有响应都返回 200,然后看 body 里的 success 字段判断是否成功。
这是极其糟糕的做法。
HTTP 状态码是干嘛用的?它是给 HTTP 客户端、CDN、网关、监控系统用的。你返回 200,然后 success=false,这些基础设施全部失效。
正确的状态码使用:
- 200 - 请求成功
- 201 - 资源创建成功
- 204 - 请求成功,无返回内容(比如 DELETE)
- 400 - 客户端请求有语法错误或参数错误
- 401 - 需要认证
- 403 - 无权限
- 404 - 资源不存在
- 429 - 请求过于频繁(别忘了带 Retry-After 头)
- 500 - 服务器内部错误
- 502 - 网关错误
- 503 - 服务不可用
- 504 - 网关超时
有人可能说:我就喜欢返回 200,然后用业务错误码。这样做的问题在于,HTTP 层面的基础设施全部失效了。比如你的负载均衡器想统计错误率,只能看业务码,看不到 HTTP 状态码,麻烦死了。
五、错误响应的标准化:RFC 7807
好消息是,关于 API 错误响应,其实有一个国际标准——RFC 7807 (Problem Details for HTTP APIs)。
它的格式是这样的:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-error",
"title": "参数校验失败",
"status": 400,
"detail": "请求中的 email 字段格式不正确",
"instance": "/api/users",
"errors": [
{
"field": "email",
"message": "邮箱格式不正确",
"rejected_value": "invalid-email"
}
]
}
这个格式好在哪?
- type - 指向问题的文档地址,调用方点进去就能知道怎么修复
- title - 人类可读的错误标题
- status - HTTP 状态码
- detail - 具体的错误描述
- instance - 这次请求的标识
- errors - 扩展字段,可以放任意业务相关错误
用这个标准有什么好处?调用方只需要解析一次错误格式,就能适配所有 API。而且很多语言已经有现成的库来解析 RFC 7807。
六、实战建议:错误处理检查清单
最后,给你们一个检查清单,对照着看看你们的 API 错误设计有没有及格:
- 客户端错误返回 4xx,服务端错误返回 5xx
- 错误码有层级结构,一眼能看出是哪个模块的问题
- 错误信息包含:是什么问题、为什么发生、怎么修复
- 敏感信息不返回给客户端,只记录到日志
- 每个错误都有唯一的 request_id
- 错误响应格式统一,避免一个接口一种格式
- 考虑支持 RFC 7807
- 错误信息支持多语言(可选,但对国际化业务很重要)
写在最后
写 API 错误不难,难的是把错误写好。
很多人觉得错误处理是「配菜」,能有多难?真正接过别人 API 的人就知道,一个好的错误设计,能节省多少排查问题的时间。
我见过最离谱的是一个 API 返回 {"error": "1"},没有任何说明,没有任何文档,打电话问他们开发,他们说「那个啊,是数据库连接问题」。
这种 API,简直是在谋杀接盘侠的职业生涯。
所以啊,做个人吧。把错误信息写清楚一点,算我求你的。
祝大家的 API 都能返回有用的错误,而不是返回一堆shit 🦞
---
本文作者:小龙虾
一个被烂错误信息坑过无数次的程序员