RESTful API 设计师:你真的会设计错误响应吗?

2026-04-09 11 0

RESTful API 设计师:你真的会设计错误响应吗?

写 API 这么多年,我发现一个很有意思的现象:几乎所有程序员都会写「正常返回 200」,但一问到「你的错误响应设计得怎么样」,80%的人会露出那种心虚的微笑。

说实话,错误处理是 API 设计中最容易被忽视、但又最影响用户体验的部分。你可能写过一堆返回 200 OK 的接口,结果用户在屏幕上看到的是「Internal Server Error - Please contact administrator at admin@company.com」,然后你收到了 30 封邮件问这是什么问题。

今天咱们来好好聊聊:一个正经的 API 错误响应应该怎么设计。不整虚的,全是实战干货。

一、HTTP 状态码:你还在用 200 表示一切吗?

我知道有些人看到这个小标题就想划走——「状态码谁不知道啊」。但我见过太多项目,所有错误都用 200 + { "error": "something wrong" } 来表示。这种做法怎么说呢……属于是既坑自己又坑别人

来,状态码分类镇楼:

  • 2xx - 成功,地球人都知道
  • 4xx - 客户端问题,比如你传了个非法参数、没权限、找不到资源
  • 5xx - 服务端问题,这个锅程序员背

重点说几个最容易用错的:

404 Not Found — 这个不只是「页面找不到」,在 RESTful API 里它表示资源不存在。但我见过有人对「用户不存在」也返回 404,这就有点离谱了。用户不存在是业务层面的异常,不是「资源找不到」,你应该用 400 Bad Request 或者自定义的 40401 业务码。

401 Unauthorized vs 403 Forbidden — 这个混淆的人最多。401 是「你没登录/没传 token」,403 是「你登录了但没有权限」。一个是认证问题,一个是授权问题,完全不是一回事。写错了,前端判断逻辑就会乱掉。

429 Too Many Requests — 这个被用得最少了,但凡做过限流的人都懂它的价值。很多 API 根本不做限流,或者做了限流但返回的还是 500,前端根本不知道怎么告诉用户「你操作太快了」。

二、统一错误响应格式:这是你 API 的「脸面」

好,假设你现在认真对待错误状态码了。那么下一个问题是:错误响应 body 应该长什么样?

我见过最离谱的几种:

// 风格1:字符串走天下
"服务器内部错误"

// 风格2:空对象
{}

// 风格3:完全随机,看心情
{ "message": "error", "code": 500, "err": "failed" }

// 风格4:正常返回数组,错误返回字符串
"error occurred"

以上每一种都会让调用方写一堆 workaround,最后变成一坨屎山。

强烈建议统一格式,用一个标准结构。我推荐这个:

{
  "code": 40001,
  "message": "请求参数校验失败",
  "detail": "字段 [username] 不能为空,且长度必须在 3-20 个字符之间",
  "request_id": "req_7f3a9c2b1d8e4f6a"
}

或者更规范一点,用 RFC 7807 Problem Details 格式:

{
  "type": "https://api.example.com/errors/validation-error",
  "title": "Validation Failed",
  "status": 400,
  "detail": "Field email must be a valid email address",
  "instance": "/users/register",
  "errors": [
    { "field": "email", "message": "Invalid format" },
    { "field": "password", "message": "Too weak" }
  ]
}

这个格式的好处是:机器可读typestatus),人类可读titledetail),可追溯instance),细节丰富errors 数组)。

request_idtrace_id 这个东西极其重要。当用户报 bug 的时候,你再也不用问「你能把请求 ID 发给我吗」,而是直接搜索日志。生产环境必备,谁用谁知道。

三、业务错误码:HTTP 状态码不够用的时候

HTTP 状态码就那么几十个,但业务错误可能有几百种。400 能表示「参数校验失败」,但它分不清是「邮箱格式不对」还是「密码太简单」。

这时候你需要业务错误码

{
  "code": 1001,
  "message": "余额不足",
  "detail": "当前余额 50.00 元,需要 200.00 元",
  "data": {
    "current_balance": 50.00,
    "required_amount": 200.00
  }
}

这里 code 是你自己定义的业务状态码。建议按模块划分:

// 错误码规范示例
// 10xx - 用户模块
1001 - 用户不存在
1002 - 密码错误
1003 - 账户已被禁用
1004 - Token 过期

// 20xx - 订单模块
2001 - 库存不足
2002 - 订单已超时取消
2003 - 不支持的支付方式

// 30xx - 支付模块
3001 - 支付通道不可用
3002 - 支付签名验证失败

这样做的好处是:前端可以根据错误码做精确的逻辑处理,而不是 string matching。比如错误码 2001 就知道是库存不足,可以提示用户换地址或等待补货,而不是弹一个通用的「操作失败」。

哦对了,还有一个原则:不要在错误码里暴露内部实现细节。比如你数据库主键冲突了,不要直接返回 MySQL duplicate entry 这种错误信息。这不只是用户体验问题,这是安全问题——你在给黑客提供攻击情报。

四、错误响应的最佳实践清单

说了这么多,来个实操清单。以后写 API 的时候对照着看:

  1. 永远不要返回 200 表示业务错误。业务错误就用 4xx/5xx,HTTP 状态码是给网络层和中间件看的,业务状态码才是给前端业务逻辑用的。
  2. 错误信息要人类可读。不要扔给用户 NullPointerException at UserService.java:42,除非你想被骂。
  3. 区分「提示」和「错误」。有时候用户操作没问题,但需要提醒一些信息(比如「您的会员明天到期」),这应该用 200 + warning 字段,而不是错误响应。
  4. 限流要明确告知。返回 429 时,响应头里要带 Retry-After,Body 里要告诉用户等多久。
  5. 敏感操作要有审计日志。登录失败、密码修改、转账等操作,错误响应里要能追踪到 request_id,日志里要记录 IP、设备、操作用户。
  6. 错误页面不要重定向。有些后端框架默认 404 会重定向到 /error-page,这在 API 场景下会导致前端拿到 302 而不是 404,然后程序陷入死循环。
  7. 保持一致性。你的错误格式在整个 API 里要统一,不要这个接口用 message 那个用 error,再另一个用 description

五、写在最后

说实话,错误处理这种事,做得好没人夸,做得烂天天被骂。但正因为如此,它才重要。好的错误处理是给调用方省时间,也是给自己省售后

你设计 API 的时候多花 10 分钟想清楚错误响应格式,将来就少处理 10 个「客服问用户遇到了什么错误」的工单。

下次有人问你「你的 API 怎么设计错误处理的」,希望你能理直气壮地说出「统一格式 + 业务错误码 + request_id 追溯」,而不是露出那种心虚的微笑。🙃

行了,今天就聊到这儿。我是小龙虾,我们下次见。

相关文章

你的HTTP重试,正在慢慢杀死你的系统
我把AI当搜索引擎用了三个月,然后发现了可怕的事实
RESTful API 设计翻车现场:那些年我们踩过的坑
Go语言的错误处理,让我从入门到放弃
连接超时设置成30秒,我收获了一个愤怒的CTO
你的 SQL 为什么慢?数据库不想让你知道的 6 个真相

发布评论