用 curl 和 Postman 发送请求

你每天都会用到的两个工具,以及何时该选哪一个。

到目前为止,你都是从浏览器和这些页面上的“发送”按钮发出请求的。这只够用来发送简单的 GET 请求。要选择方法、设置请求头或发送请求体,你需要一个真正的客户端。几乎每个做 API 测试的人最后都会用两个:命令行里的 curl,以及像 Postman 这样的图形化客户端。这一课会把两者各讲到足够你完成本教程的剩余部分。

curl

curl 是一个命令行程序,它发送请求并打印响应。macOS、大多数 Linux 系统以及 Windows 10 和 11 都默认安装了它。用 curl --version 检查一下。它也是开发者分享请求时使用的格式:API 文档、浏览器开发者工具和 Postman 都能把请求显示或导出为 curl 命令,所以即使你更喜欢别的工具,也需要能读懂它们。

不加任何选项时,curl 只打印响应体。加上 -i 就会同时显示状态行和响应头,测试时你几乎总是需要它们:

Terminal
curl -i https://api.testingapis.com/api/health
输出
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
date: Sun, 11 Oct 2026 11:47:45 GMT
server: Kestrel
x-powered-by: TestingAPIs
transfer-encoding: chunked

{"status":"healthy","uptimeSeconds":76890,"timestamp":"2026-10-11T11:47:46Z"}

想看更多细节,-v(详细模式)还会打印 curl 发送的请求(用 > 标记)和响应头(用 < 标记)。以 * 开头的行描述的是连接。当请求的行为不符合预期,而你想准确看到网络上传输了什么时,就用它。

Terminal
curl -v https://api.testingapis.com/api/health
输出(节选)
* Host api.testingapis.com:443 was resolved.
* IPv4: 135.13.233.203
* Established connection to api.testingapis.com (135.13.233.203 port 443)
* using HTTP/1.x
> GET /api/health HTTP/1.1
> Host: api.testingapis.com
> User-Agent: curl/8.18.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< content-type: application/json; charset=utf-8
< date: Sun, 11 Oct 2026 11:49:47 GMT
< transfer-encoding: chunked
...

curl 添加了三个你没有要求的请求头:Host、User-Agent 和 Accept。每个客户端都会添加一些默认请求头,而不同的客户端添加的各不相同。当同一个请求在 Postman 里能用、在 curl 里却失败时,原因往往就是某个默认请求头。

方法、请求头和请求体

三个选项就能覆盖大多数请求。-X 设置方法。-H 添加一个请求头,可以重复使用。-d 发送请求体。下面向一个会原样返回所收内容的端点发送一个带 JSON 请求体的 POST:

Terminal
curl -X POST https://api.testingapis.com/api/methods/post \
  -H "Content-Type: application/json" \
  -d '{"name":"Asha","role":"tester"}'
输出
{"method":"POST","message":"POST request received","path":"/api/methods/post","query":{},"contentType":"application/json","bodyLength":31,"bodyTruncated":false,"body":"{\"name\":\"Asha\",\"role\":\"tester\"}","timestamp":"2026-09-30T18:31:16Z"}

每行末尾的反斜杠让命令在 bash 和 zsh 中延续到下一行。请求体用单引号括起来,这样 shell 就不会动里面的双引号。如果你忘了 Content-Type 请求头,curl 会发送 application/x-www-form-urlencoded,因为 -d 本来是为 HTML 表单设计的,很多 API 会拒绝这样的请求体。

curl 7.82 及以后的版本有一个快捷方式 --json,它会发送请求体,设置 Content-Type: application/json 和 Accept: application/json,并把方法切换为 POST。下面这条命令会创建一个真实的待办:

Terminal
curl --json '{"title":"Learn curl","completed":false,"userId":1}' \
  https://api.testingapis.com/api/Todos
输出
{"message":"Todo created successfully","data":{"id":31,"userId":1,"title":"Learn curl","completed":false,"priority":"medium","dueDate":""}}

你拿到的 ID 会不一样,因为其他人也在创建待办。对于 PUT 或 PATCH,在 --json 旁边加上 -X PUT 或 -X PATCH。

计时和脚本

-w 会在响应之后打印 curl 测量到的数值。配合 -s(静默,不显示进度条)和 -o /dev/null(丢弃响应体),就能得到一行摘要:

Terminal
curl -s -o /dev/null \
  -w "status %{http_code}  total %{time_total}s  size %{size_download} bytes\n" \
  https://api.testingapis.com/api/Products
输出
status 200  total 0.382512s  size 5846 bytes

在 Windows 上,用 -o NUL 代替 -o /dev/null。time_total 包含 DNS 查询、建立连接和 TLS 的时间,所以第一个请求往往比后面的慢。性能测试那一课会进一步拆解这个数字。

在 Windows 上使用 curl

Windows 需要单独一节,因为同一条命令在不同的 shell 中可能有三种不同的表现。

在 Windows PowerShell 5.1(Windows 自带的版本)中,curl 并不是 curl。它是 Invoke-WebRequest 的别名,这是一个选项完全不同的 PowerShell 命令,所以 curl -i 会报出一个令人费解的错误。输入 curl.exe 才能运行真正的程序。PowerShell 7 去掉了这个别名,但输入 curl.exe 在两者中都能用,所以这是个好习惯。

引号是第二个问题。Windows PowerShell 5.1 把参数传给 curl.exe 这类程序时,会去掉参数中的双引号。下面的命令看起来没问题,服务器收到的却是损坏的 JSON:

PowerShell 5.1
PS> curl.exe -s -X POST https://api.testingapis.com/api/methods/post -H "Content-Type: application/json" -d '{"name":"Asha"}'
{"method":"POST", ... "bodyLength":11, ... "body":"{name:Asha}", ...}

请求体到达时变成了 {name:Asha},这不是 JSON。PowerShell 7.3 及以后的版本能正确传递引号。在更早的版本上,最省事的办法是把请求体放进一个文件,然后用 @ 让 curl 读取它。"@body.json" 外面的引号是为了防止 PowerShell 把 @ 当成它自己的语法。

PowerShell
# body.json contains {"name":"Asha","role":"tester"}
curl.exe -s --json "@body.json" https://api.testingapis.com/api/methods/post

在老式的命令提示符(cmd.exe)中,单引号没有任何意义,所以要用双引号括住请求体,并用反斜杠转义里面的双引号。续行符是 ^ 而不是反斜杠:

命令提示符
curl -X POST https://api.testingapis.com/api/methods/post ^
  -H "Content-Type: application/json" ^
  -d "{\"name\":\"Asha\",\"role\":\"tester\"}"

Postman 和其他图形化客户端

图形化客户端做的事和 curl 一样,只是用表单代替了命令:从下拉框选择方法,输入 URL,在各个标签页中填写请求头和请求体,然后点击 Send。响应会显示在下方,经过格式化,并附带状态码、耗时和大小。Postman 用得最广。Insomnia 和 Bruno 的用法相同,而 Bruno 把所有内容都存成普通文件,有些团队更喜欢这样,因为请求可以和代码放在同一个 Git 仓库里。下面的概念适用于所有这些工具,名称以 Postman 为准。

集合

集合是一组保存起来的请求,通常每个 API 一个,里面还可以有文件夹。对于一个你要做好几个月的项目,这正是选择图形化客户端而不是 curl 的理由:请求只需搭建一次,然后把集合分享给团队。

你不必手动搭建。这个 API 在 https://api.testingapis.com/openapi/v1.json登录后查看 发布了一份 OpenAPI 文档,它以机器可读的方式描述了每个端点。在 Postman 中选择 Import,粘贴这个 URL,它就会创建一个为每个端点各准备一个请求的集合。你在工作中测试的大多数 API 都会有类似的文件,这是上手一个陌生 API 最快的方式。

环境和变量

真实的项目会在多个地方运行同一个 API:开发者的笔记本、测试服务器、预发布环境、生产环境。除了基础 URL 和凭据之外,请求都是一样的。Postman 用环境来处理这个问题。创建一个名为“testingapis”的环境,其中变量 baseUrl 设为 https://api.testingapis.com,然后把请求的 URL 写成 {{baseUrl}}/api/Products/1。切换环境就会一次性切换所有请求。令牌也放进变量里,这样就不会被复制到几十个请求中。

Postman 中的测试

每个请求都有一个 Tests 标签页(在较新的版本中叫 Scripts,里面有 post-response 部分),你可以在其中编写在响应到达后运行的 JavaScript。对于 GET {{baseUrl}}/api/Products/1:

Postman 测试
pm.test("status is 200", function () {
  pm.response.to.have.status(200);
});

pm.test("product 1 has a price", function () {
  const product = pm.response.json();
  pm.expect(product.id).to.eql(1);
  pm.expect(product.price).to.be.a("number");
});

结果会显示在响应旁边的 Test Results 标签页中。之后,Collection Runner 可以运行集合中的每个请求,并报告哪些检查失败了。这是迈向自动化的平缓第一步。自动化那一课会讲如何把测试写成普通代码,当测试数量超过几十个时,这种方式扩展性更好。

该用哪一个

两个都用,各司其职。

场景更好的选择
快速检查一件事,或复现一份 bug 报告curl
在工单或聊天中分享一个精确的请求curl(任何人都能粘贴运行)
探索一个你还不熟悉的 API导入 OpenAPI 的图形化客户端
需要反复编辑的长 JSON 请求体图形化客户端
在测试服务器和预发布服务器之间切换图形化客户端的环境
在脚本或构建服务器上运行curl,或用代码编写的测试
查看原始请求头和连接细节curl -v

针对这个 API 还有一个选择:API 参考文档中每个端点的页面都有一个用来发送请求的表单,并显示一条可以直接复制的 curl 命令。在把端点保存到任何地方之前,这往往是试用它最快的方式。

  • 当 bug 报告中包含请求时,把它作为 curl 命令附上。开发者无需导入任何东西就能运行它,而且发送了哪些请求头也一目了然。
  • Postman 可以双向转换。把 curl 命令粘贴到 Import 对话框中就能得到一个请求,或者使用请求上的 Code 按钮得到 curl。

练习

先用 curl 完成下面的练习,然后在图形化客户端中再做一遍。

  • 发送 GET /api/echo/headers,比较 curl 发送的请求头和你的图形化客户端发送的请求头。每个工具各自添加了哪些请求头?
  • 用 POST /api/Todos 创建一个待办,然后用 GET /api/Todos/ 加上响应中的 ID 来获取它。
  • 发送同样的 POST,但不带 Content-Type 请求头。返回了什么状态码和响应体?
  • 用 -w 给 /api/Products 和 /api/Products?delay=2 计时。delay 参数会让服务器等待,所以第二个应该多花大约两秒。

在你的图形化客户端中,创建一个带 baseUrl 变量的环境,并写一个故意失败的测试,这样在依赖测试通过之前,你就知道失败是什么样子。

练习与延伸