在我终端里,curl 是我敲得最多的命令,没有之一。API 一抽风,第一个跑的是它;部署要不要验一下,最后一个跑的也是它;想向自己证明“线上到底传了什么”,靠的还是它。这是一份实用的 curl 教程——不是那本厚到离谱的 man page,而是我真正在用的 curl flags、每个 flag 会打印什么,以及请求出问题时该怎么读输出。如果问题其实在 HTTP 之下,那同一个请求会出现在抓包里:那就是读懂 tcpdump 与 Wireshark。
下面每个例子都真实跑在一个 127.0.0.1:8731 的小 Python 服务上,所以数字都是真的,输出只是做了裁剪,没有编造。
看清整个请求:-v、-i 与 -D
如果只能学一个 flag,就学 -v。它会打印连接过程、请求头和响应头,并且每一行都带了前缀,让你永远知道数据往哪个方向走:
$ curl -sv http://127.0.0.1:8731/
* Trying 127.0.0.1:8731...
* Established connection to 127.0.0.1 (127.0.0.1 port 8731)
* using HTTP/1.x
> GET / HTTP/1.1
> Host: 127.0.0.1:8731
> User-Agent: curl/8.22.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 36
< X-Demo: curl-cookbook
<
hello from the curl cookbook server
* Connection #0 to host 127.0.0.1:8731 left intact
看三种前缀:* 是 curl 在跟你说话(连接建立、协议、连接复用),> 是 curl 发出去的,< 是服务端返回的。空的 > 和 < 行表示这一段 header 结束。十次里有九次,bug 就摆在这儿——少了个 Content-Type、Host 跟 vhost 对不上、本该 200 却来了个 302。
只想要响应时,-i 会把响应头和 body 一起打印,而 -D 把响应头写进文件、让 body 保持干净:
# -i:先头后体,直接打到终端
curl -si http://127.0.0.1:8731/ | head -6
# -D:头存一个文件、体存另一个,之后好 grep
curl -sS -o body.txt -D headers.txt http://127.0.0.1:8731/
脚本里我默认用 -D:grep -i 'cache-control\|etag' headers.txt 立刻告诉你这个响应能不能被缓存、有没有校验器——这正是 HTTP 缓存的全部关键。
curl -w timing:让 curl 告诉你时间花在哪
-w(write out)能把 curl 变成一个计时器。%{time_total} 是最抢眼的那个,但真正有用的是拆分:time_namelookup(DNS)、time_connect(TCP)、time_starttransfer(首字节时间,TTFB)和总时间。
$ curl -sS -o /dev/null \
-w 'code=%{http_code} ttfb=%{time_starttransfer}s total=%{time_total}s\n' \
http://127.0.0.1:8731/slow
code=200 ttfb=0.400861s total=0.400899s
服务端故意睡了 400 毫秒,这个拆分就讲清了故事:TTFB 和 total 几乎一样,说明时间全在服务端,跟传输无关。反过来,如果 time_connect 很大,你就该怀疑网络;如果 time_namelookup 很大,那就是 DNS。这一行,在你伸手去拿别的工具之前,就已经把“我的代码慢”和“网络慢”分开了。
把格式串存下来,省得每次重敲:
# -w @file 从文件里读格式串
printf 'http_code=%%{http_code} ttfb=%%{time_starttransfer}s total=%%{time_total}s\n' > wfmt.txt
curl -sS -o /dev/null -w @wfmt.txt http://127.0.0.1:8731/
重定向:-L 与 --max-redirs
curl 默认不跟随重定向。不加 -L,你只会拿到那个 3xx 和一个 Location 头,别的什么都没有:
$ curl -sS -o /dev/null -D - http://127.0.0.1:8731/redirect
HTTP/1.1 302 Found
Location: /
加上 -L 才会跟着跳,再用 %{num_redirects} 数一下跳了几次。同一个 URL,加与不加:
$ curl -sSL -o /dev/null -w 'code=%{http_code} redirects=%{num_redirects}\n' \
http://127.0.0.1:8731/redirect
code=200 redirects=1
--max-redirs 给跳转链设一个上限——当重定向循环正在吃你时间时特别有用,它会给一个干净的 exit code,而不是无限地打下去:
$ curl -sSL --max-redirs 0 -o /dev/null http://127.0.0.1:8731/redirect
curl: (47) Maximum (0) redirects followed
$ echo $?
47
错误与 exit codes:--fail、-o 与 -sS
默认情况下,哪怕返回 404,curl 也是退出 0——它成功地把字节送到了,所以在 curl 眼里什么都没失败。对脚本来说,这恰恰是错的。--fail 会把 4xx/5xx 变成一个非零退出码:
$ curl -sS -o /dev/null -w 'code=%{http_code}\n' http://127.0.0.1:8731/missing
code=404
$ echo $? # curl 很满意:它拿到响应了
0
$ curl -sS --fail -o /dev/null http://127.0.0.1:8731/missing
curl: (22) The requested URL returned error: 404
$ echo $?
22
exit codes 本身就是一套 API:6 是“域名解析不了”,7 是“连不上”,22 是“配了 --fail 的 HTTP 错误”,28 是超时,47 是重定向太多。连接被拒时会直白地告诉你:
$ curl -sS -o /dev/null http://127.0.0.1:9/
curl: (7) Failed to connect to 127.0.0.1 port 9: Could not connect to server
$ echo $?
7
两个习惯能让脚本安全。-o FILE 把 body 送进文件,这样进度条或载荷永远不会混进 stdout;-sS 表示silent 但保留错误——不要进度条,但真实失败照常打印。单用 -s 会把错误信息也一起藏掉,这正是人们最后盯着一个 0 字节文件、完全不知道为啥的原因。
方法与请求体:-X、-d、--json 与 -F
-d 发送请求体并隐含 POST。发 JSON 时,--json 比手写 header 更短,而且它把两件事都做对了:设置 Content-Type: application/json 和 Accept: application/json。
$ curl -sS --json '{"hello":"world"}' http://127.0.0.1:8731/echo
{
"received": "{\"hello\":\"world\"}",
"content_type": "application/json",
"user_agent": "curl/8.22.0"
}
表单上传用 -F,它会构造 multipart/form-data,文件也能带上:
curl -sS -F 'file=@report.pdf' -F 'note=hello' https://api.example.com/upload
-X 只在需要覆盖方法时才用(-X DELETE、-X PUT)。别写 -X POST 配 -d——-d 已经隐含它了,而手动 -X 有可能让你意外发出一个带 body 的 GET。
请求头:-H、-A 与 -e
-H 能加或替换任意 header,并且可以重复。-A 设置 user agent、-e 设置 Referer,两者都是 -H 的简写:
$ curl -sv -H 'X-Trace: abc123' -A 'cookbook/1.0' \
-e 'https://blog.awin-x.top/' http://127.0.0.1:8731/ 2>&1 | grep '^> '
> GET / HTTP/1.1
> Host: 127.0.0.1:8731
> User-Agent: cookbook/1.0
> Accept: */*
> Referer: https://blog.awin-x.top/
> X-Trace: abc123
想发一个空 header(有些服务端需要没有值的 Accept-Encoding:),用带结尾分号的 -H 'Name;'。
认证:-u 与 --netrc
-u user:pass 走 HTTP Basic 认证。curl 会把它变成一个 base64 的 Authorization 头——你可以用 -v 亲眼看到它发生:
$ curl -sv -u alice:s3cret http://127.0.0.1:8731/ 2>&1 | grep -i authorization
> Authorization: Basic YWxpY2U6czNjcmV0
把密钥写在命令行上,会泄漏进 shell history 和 ps。--netrc 改成从文件读凭据,--netrc-file 可以指定具体哪个文件:
# ~/.netrc — chmod 600
machine api.example.com
login alice
password s3cret
curl -sS --netrc-file ~/.netrc https://api.example.com/private
TLS、HTTP/2 与 HTTP/3:--http2、--http3、-k、--cert
curl -V 列出你这套构建能讲哪些协议;我这边印着 HTTP2 HTTP3:
Features: alt-svc AsynchDNS brotli ... HTTP2 HTTP3 HTTPS-proxy ... TLS-SRP
--http2 通过 ALPN 提议 HTTP/2,服务端不接受就退回 HTTP/1.1;--http3 走 UDP 上的 QUIC。两者都取决于服务端,所以对着我这个 HTTP/1.1 测试服务,协商结果就是落在 HTTP/1.x——就是上面那行 * using HTTP/1.x。这些 flag 改的是 curl 提议什么,不是服务端接受什么。
-k(或 --insecure)跳过证书校验。它只是调试工具:用它确认一次是不是证书问题,然后回去修信任链。客户端证书用 --cert 和 --key:
# 排查:到底是 TLS 的问题,还是应用的问题?
curl -vk https://self-signed.local:8443/health
# 双向 TLS,带客户端证书
curl --cert client.pem --key client.key https://mtls.example.com/api
连接复用与 --resolve 测试
curl 在同一次调用里会复用连接。--resolve host:port:ip 让你把某个主机名指向某个 IP,而不用改 DNS 或 /etc/hosts——在解析记录切换之前测试新后端,这是最有用的一招:
$ curl -sS --resolve demo.local:8731:127.0.0.1 \
-w 'code=%{http_code} ip=%{remote_ip}:%{remote_port}\n' \
http://demo.local:8731/
code=200 ip=127.0.0.1:8731
%{remote_ip} 会确认 curl 确实连到了你指定的地方。我就是在生产流量还指向别处时,用真实主机名去验证 TLS、vhost 和一次全新部署的。
调试 HTTP 请求:为什么 -v 比瞎猜管用
值得第一个伸手去拿 -v,是因为 HTTP 在某种意义上是一种能穿透 TLS 的文本协议:curl 自己解密自己的连接,所以即使走 HTTPS,-v 也能让你看到请求和响应确切的字节。瞎猜(“是不是认证头的问题”“是不是被缓存了”)要花好几分钟;一次 -v 只要几秒。
一个能发现大多数 bug 的节奏:
curl -v——请求是不是你以为的样子?URL、方法、headers、body 对不对?- 看响应行(
200、302、403、500),然后看 headers,尤其是Location、Content-Type、Cache-Control。 - 加上
-w '%{http_code} %{time_total}s\n'——是不是慢?慢在哪? - 涉及重定向就加
-L;需要脚本在意结果就加--fail。 - 只有当线路本身看着不对劲时,才去抓包。
一张速查表
-v看请求/响应,-i连头带体,-D把头写进文件。-w '%{time_total}s'以及它的兄弟们,用来拆分 DNS、连接和 TTFB。-L跟随重定向,--max-redirs给它设限。--fail让 HTTP 错误变成非零退出码;-sS安静,但出错时照喊。--json发 JSON,-F发 multipart,其余的用-d。- 应急用
-u,正经事用--netrc-file。 --resolve把主机名指到指定 IP 测试,不碰 DNS。
这些都不冷门。它们就是那二十来个 flag,把 curl 从“发个请求”变成“把到底发生了什么,一字不差地告诉我”——而这,正是我们伸手去拿它的全部意义。