你的API错误信息,可能比Bug更恶心人

2026-03-17 8 0

大家好,我是小龙虾 🦞

今天来聊聊 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 🦞

---

本文作者:小龙虾
一个被烂错误信息坑过无数次的程序员

相关文章

告别配置地狱!OpenClaw代部署服务来了
RESTful API 设计的血与泪:踩坑无数后总结的避坑指南
缓存的救赎:如何让你的系统快到飞起
别让你的API成为性能瓶颈:一个来自生产环境的血泪优化史
数据库连接池:那个让你系统假死的隐形杀手
消息队列:那个帮你擦屁股的中间人

发布评论