📘 第四章 返回结果的HTTP状态码
📄 一句话概括

200 只说明「请求被理解、被正常处理」。 它不保证业务成功——业务失败也回 200 是最大的接口设计坑。

📝 本节和前面几节的关系

上一节分了家族。这一节看最常见的那个码,以及它被滥用的方式。

一、200 的边界

200 说的只是协议层的事:我收到了、我看懂了、我处理了,没出错。 它不保证内容是最新的(可能是缓存),也不保证业务成功。 最常见的滥用是「业务失败也回 200」:200 {"code": 500, "msg": "余额不足"}。 这在协议上没错,但在工程上把整条链路搞坏了。

二、滥用之后坏在哪

监控全瞎:看 5XX 比例永远是 0,故障发现不了。 重试逻辑失效:客户端不知道哪些该重试。 缓存乱套:200 可以被缓存,失败的响应被缓存下来更难查。 网关误判:负载均衡、CDN 都靠状态码决定怎么处理。 正确做法:失败就用 4XX/5XX,正文放业务码和说明。 如果非要在正文里带业务码,那 HTTP 状态码也必须跟着变。

三、200 与 201/204 的分工

201 表示「新建了资源」,必须带 Location 指向它。 204 表示「成功了但没内容」——它和 200 空正文的区别是语义:204 是「本来就没有」, 200 空是「有但空」。 这三个码别混用。客户端对 204 不会去解析正文,对 200 会; 某些框架对 200 空正文会抛解析异常,对 204 不会。

四、动手 Demo:发一个最普通的 200

服务端回 200 加一段 UTF-8 中文,把状态行、正文、Content-Type 逐项打出来。

def resp(raw, st):
    return (u'HTTP/1.1 200 OK',
            [(u'Content-Type', u'text/plain; charset=utf-8')],
            u'正常内容'.encode())

实跑输出:

--------------------------------------------------------------------
200 OK:最常见的那一个
--------------------------------------------------------------------
  状态行                     HTTP/1.1 200 OK
  正文                       正常内容(12 字节)
  Content-Type               text/plain; charset=utf-8
  整段响应                   111 字节

  200 只说明什么             请求被理解、被处理了,且结果正常
  200 不说明什么             不保证内容是最新的、不保证业务逻辑成功
  最常见的坑:业务失败也回 200。接口返回 `200 {"code": 500, "msg": "失败"}`——
  这是把业务状态塞进了正文,HTTP 层的监控、缓存、重试全部失效。
  正确做法是用 4XX/5XX 表达失败,正文只放细节。

正文 正常内容(12 字节)——四个汉字在 UTF-8 里是 12 字节,Content-Length 也必须是 12。 编码与长度必须一致,写成 4 客户端就只拿到一个汉字。 Content-Type ... charset=utf-8 里的 charset 不能省。省了浏览器会按猜测来解中文,猜错就是乱码。 整段响应 111 字节,正文只占 12——短响应的固定开销占了九成。

30 秒实验

  1. 把正文换成 4 个英文单词,重跑,看 Content-Length 变成多少。
  2. 故意把 Content-Length 写成 4,看客户端收到什么。
  3. 把 charset 去掉,用浏览器打开同一段字节,看会不会乱码。

常见误区

  1. 「200 表示业务成功」——它只表示协议层没出错。
  2. 「业务码写在正文就够了」——HTTP 状态码也得跟着变,否则监控和网关全瞎。
  3. 「200 空正文和 204 一样」——语义不同,客户端行为也不同。
自测三问(点开对答案) 1. 200 到底保证了什么? —— 请求被理解、被正常处理,没有协议层错误。业务是否成功它不管。 2. 「业务失败回 200」最大的危害是什么? —— 监控看不到错误率,重试和缓存也全都失效。 3. 新建资源该回哪个码? —— 201 Created,并且要带 `Location` 指向新资源。

200 只表示有内容。下一节看「成功了但没内容」的那个码。

相关笔记