读懂响应

状态码、响应头和响应体。

每个请求都会得到恰好一个响应(除非连接断了,那本身也是一种回答,会在后面的课程中讲到)。一个响应由三部分组成:状态行、响应头和响应体。刚开始做测试的人往往只看响应体,因为数据都在那里。而漏网的 bug 大多藏在另外两部分里。

响应长什么样

和请求一样,HTTP/1.1 响应也是纯文本。下面是 GET /api/health 的响应,用 curl 的 -i 选项(它会把响应头和响应体一起打印出来)显示的样子。去掉了几个基础设施相关的响应头。

原始响应
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
x-powered-by: TestingAPIs

{"status":"healthy","uptimeSeconds":3542,"timestamp":"2026-09-30T18:28:12Z"}

第一行是状态行:协议版本、三位数的状态码和一个简短的原因短语。接着是响应头,每行一个,然后是一个空行,最后是响应体。原因短语只是给人看的,客户端和测试应该看数字。HTTP/2 完全去掉了原因短语,所以一个检查文本 "OK" 的测试可能会在服务器升级后失败。

状态码的类别

状态码的第一位数字告诉你响应的类别。很多时候,仅凭这一位数字就能判断结果。

类别含义是谁的问题
1xx信息。请求仍在处理中。谁的都不是。你很少直接看到它们。
2xx成功。服务器完成了你的要求。谁的都不是。
3xx重定向。去别处找。谁的都不是。客户端需要继续跟进。
4xx客户端错误。请求有问题。客户端:输入错误、没有权限、URL 不对。
5xx服务器错误。请求本身可能没问题。服务器,或者它背后的某个东西。

最后一列正是状态码在测试中如此重要的原因。4xx 的意思是“修改你的请求”,5xx 的意思是“我们这边出了问题”。如果你发送了乱七八糟的内容却得到了 500,即使请求本身是错的,这也是一个 bug,因为服务器本该用 400 礼貌地拒绝这个输入,却直接崩溃了。找出这些 500,是刚入门的测试人员能做的最有成效的事情之一。

最常见的状态码

状态码名称典型用途
200OK请求成功,响应体中有结果。
201Created一个 POST 创建了新记录。
204No Content成功了,并且有意不返回响应体。DELETE 之后很常见。
301 / 302Moved Permanently / Found资源位于 Location 响应头给出的 URL。
304Not Modified你缓存的副本仍然是最新的。会在缓存一课中讲到。
400Bad Request请求格式错误,或某个值无效。
401Unauthorized没有凭据或凭据错误。尽管名字如此,它的意思是未认证。
403Forbidden凭据没问题,但这个用户无权执行此操作。
404Not Found这个 URL 上什么都没有,或者没有这个 ID 的记录。
405Method Not AllowedURL 存在,但不支持这个方法。
409Conflict请求与当前状态冲突,比如邮箱重复。
422Unprocessable Content语法没问题,但数据违反了某条规则。
429Too Many Requests你触发了限流。放慢速度。
500Internal Server Error服务器上发生了未处理的错误。
502Bad Gateway代理从它背后的服务器收到了错误的响应。
503Service Unavailable服务器过载或正在维护。

不同的 API 对其中一些状态码有不同的用法。有的团队对所有校验错误都返回 400,有的则用 422。有的删除操作返回 200 并在响应体里附上消息,有的返回不带任何内容的 204。两者都没有错。你要检查的是:API 是否符合它自己的文档,以及不同端点之间是否保持一致。

试试各种状态码

/api/status/{code} 会用你放在路径里的状态码来响应。用它可以观察某个工具或客户端库在收到特定状态时的表现。试几个看看:

GET/api/status/201
GET/api/status/503

在 curl 里,503 看起来是这样的:

响应
HTTP/1.1 503 Service Unavailable
Content-Type: application/json; charset=utf-8
retry-after: 5

{
  "status": 503,
  "statusText": "Service Unavailable",
  "category": "server-error",
  "message": "This is a 503 response. Use /api/status/{code} to request any status."
}

注意 retry-after: 5 这个响应头。503 或 429 经常会告诉客户端在重试之前要等待多少秒,行为良好的客户端会遵守它。你要检查的是这个响应头是否存在,而不只是状态码。

超出有效范围的状态码会被拒绝。/api/status/999 返回 400,消息是 Status code must be between 200 and 599 (1xx cannot be sent as a final response). 而 /api/status/abc 返回一个响应体为空的 404,因为 abc 根本匹配不上这条路由。两个错误的输入,两种不同的失败方式。你会经常看到这种模式:类型不对的值往往在路由层就失败了,应用代码根本没有机会运行。

重定向

3xx 响应通常没有有用的响应体。重要的是 Location 响应头,它说明接下来要去哪里:

GET /api/status/302 的响应
HTTP/1.1 302 Found
Content-Type: application/json; charset=utf-8
location: /api/echo

浏览器和大多数 HTTP 库会自动跟随重定向,只给你看最终的响应。这很方便,直到你需要测试重定向本身的时候。curl 除非加上 -L 否则不会跟随重定向,所以它更适合做这件事。

响应头

响应头描述响应体,并给客户端下达指示。请求商品列表的第 3 页,每页两个商品:

GET/api/Products?limit=2&page=3
响应头
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
x-page: 3
x-per-page: 2
x-total-count: 20
x-total-pages: 10

Content-Type 说明响应体是 UTF-8 编码的 JSON。x-page 和 x-total 这四个响应头携带分页的细节:共有 20 个商品、10 页,这是第 3 页。响应体只是一个包含两个商品的数组,所以忽略这些响应头的客户端根本无从得知后面还有更多。以 X- 开头的响应头是 API 自己定义的自定义响应头,由 API 而不是 HTTP 标准来说明。

有些响应头用 Unix 时间戳表示时间,即从 1970 年 1 月 1 日(UTC)起经过的秒数。带限流的 API 常常在 x-ratelimit-reset 中发送一个时间戳,告诉你额度何时重置。信任它之前先转换一下:重置时间在过去,或者在好几年之后,这种 bug 只有在你认真读响应头而不是跳过它们时才能发现。(时间戳转换工具可以帮你转换。)

你也许会期待一个 Content-Length 响应头。这个 API 大多改为发送 Transfer-Encoding: chunked,意思是响应体分块发送,总大小不会提前告知。两种方式都合法。一个响应只会有其中一种,不会两者都有。

响应体

响应体就是数据:一个商品、一个列表、一条错误消息、一张图片。Content-Type 说明如何读取它。你的大多数检查都会针对响应体,读懂 JSON 会详细讲解。在那之前,先看两个特殊情况。

完全没有响应体

204 表示成功且有意不带响应体。响应也没有 Content-Type,因为没有什么需要描述:

GET/api/formats/no-content
这个请求使用的是面向已登录成员的端点。登录创建免费账户
响应
HTTP/1.1 204 No Content
x-powered-by: TestingAPIs

总是调用 response.json() 的客户端代码在这里会崩溃,因为空字符串不是合法的 JSON。如果你编写会解析每个响应的测试辅助函数,它们需要能处理空响应体。

当响应头说谎时

状态、响应头和响应体由服务器的不同部分生成,没有任何机制强制它们保持一致。下面这个响应声称自己是 HTML:

GET/api/formats/wrong-content-type
这个请求使用的是面向已登录成员的端点。登录创建免费账户
响应
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

{"id":1,"name":"Alice Johnson","email":"alice@example.com","roles":["admin","editor"],"active":true,"score":98.5,"manager":null}

响应体是完全正常的 JSON,人读起来根本不会发现问题。浏览器会试图把它当作网页渲染,有些 HTTP 库会拒绝把它解析为 JSON,或者把它作为字符串返回。这个端点是故意做坏的。在真实系统中,同样的 bug 通常来自代理生成的错误页,或框架的默认设置。你的测试应该显式检查 Content-Type。

反过来的情况也会发生:状态码是 200,响应体却写着 {"success": false}。有些 API 就是这样报告所有错误的。统计 5xx 响应的监控工具永远看不到这些失败,只检查状态码的测试也一样。如果某个 API 这样做,你的测试就必须读取每个响应的响应体。

每个响应都要检查什么

在看任何一个数据字段之前,先确认基本情况。这只需要几秒钟,却能发现数量惊人的 bug。

  • 状态码是文档针对这种情况承诺的那个。
  • Content-Type 与你实际收到的响应体相符。
  • 文档提到的响应头(分页、Location、Retry-After)存在且合理。
  • 响应体可以解析,并且结构符合你的预期。
  • 错误响应与这个 API 的其他错误使用相同的格式。

练习

对每个请求,记下状态码、Content-Type,以及一句关于响应体的描述。

  • /api/status/418。查一查这个状态码为什么存在。
  • /api/formats/empty登录后查看。它和 /api/formats/no-content登录后查看 有什么不同?一个返回 200,一个返回 204。你认为客户端更容易处理哪一个?
  • /api/Products?limit=2&page=99。并没有第 99 页。你得到了什么状态和响应体?分页响应头说了什么?你会这样设计吗?
  • /api/status/list。在列表中找一个标记为 "supported": false 的状态码,试着请求它。

练习与延伸