Geonode logo
Geonode Team

Geonode Team

更新于:2026年10月7日

发布于:2026年9月2日

如何使用 curl 显示响应头

curl 默认会隐藏响应头。有四个选项可以显示这些响应头,它们之间的差异比文档中描述的更为重要。 其中一个选项会发送与您试图调试的请求不同的请求,这可能会让您浪费一个小时去追查一个根本不存在的差异。 本指南将介绍这四个选项,以及大多数人不知道的 JSON 输出,并说明通过代理时情况会发生怎样的变化。

我们之所以关注这一点:因为我们是 Geonode,我们销售代理服务,而响应头是解答客户最常问的问题——“这是代理问题还是不是?”的最快途径。在浏览器中,被封锁页面、速率限制和真实错误看起来完全一样,但在响应头中却截然不同。 如果 429 返回的响应包含 Retry-After,说明你的请求速度过快,此时任何代理都无法解决这个问题。如果 403 包含 security-vendor 标头,则说明目标服务器已识别出你的身份。 407 表示代理服务器需要凭据。在进行任何更改之前先阅读请求头,可以省去大量猜测。curl 针对这种情况提供了一个标志——%{proxy_used}(在 8.7.0 版本中添加),如果传输经过代理,该标志将返回 1。当你无法确定配置是否生效时,此功能非常有用。

四种选项一览

选项显示发送最适合
-i响应头 + 正文您的实际请求日常检查
-I仅响应头HEAD 请求快速检查(需注意)
-D file将响应头写入文件您的实际请求脚本编写、流分离
-v请求和响应头您的实际请求调试您发送的内容

关键的一行是第二行,这也是该领域造成最多困惑的根源。其余内容仅涉及输出流向的问题。

-i

:包含正文的响应头 日常使用的选项。在 curl 手册 中,该选项被描述为 -i, --show-headers :“在输出中显示响应头……此选项会将响应头与数据保存在同一个流/输出中。”

curl -i https://example.com
HTTP/2 200
content-type: text/html; charset=UTF-8
content-length: 1256
cache-control: max-age=604800
date: Wed, 02 Sep 2026 10:14:22 GMT

<!doctype html>...

首先是响应头,接着是一行空行,然后是响应正文——与网络传输格式完全一致。

关于命名的一点说明,供阅读旧资料的人参考:长格式现在是 --show-headers 。以前是 --include ,两者都有效,但当前文档使用的是新名称。

有两点细节值得注意。当输出发送到终端时,curl 可能会将标头名称设置为粗体,并标记 Location: 格式的 URL,这在交互式操作中有帮助,但在管道处理中则是不需要的——使用 --no-styled-output 可禁用此功能。此外,由于标头和正文共享同一数据流,因此 -i 配合 -o file 会将两者都写入文件,这几乎绝非用户所期望的结果。若需此功能,请使用 -D 。

-I:仅获取头部,以及为何这可能会产生误导

-I 的文档说明如下:“仅获取头部。HTTP 服务器支持 HEAD 命令,该方法利用此命令仅获取文档的头部信息。”

请仔细阅读。它并非先获取响应再丢弃正文。 它发送的是另一种 HTTP 方法。

curl -I https://example.com

这实际上是一个 HEAD 请求,且其后果是真实存在的:

某些服务器对 HEAD 的处理方式不同。 HEAD 可能会返回不同的头部信息、不同的状态码,甚至可能因 405 Method Not Allowed 而被完全拒绝——而等效的 GET 却能完美运行。

某些框架不会为 HEAD 请求计算请求体,因此 Content-Length、ETag 和 Content-Type 可能缺失或不正确。

CDN 和缓存通常将 HEAD 视为一个独立的缓存键,因此缓存标头可能与真实请求所看到的不同。

反机器人系统可能会做出不同的响应。 来自异常客户端的 HEAD 请求本身就是一种信号,而您收到的验证码可能与 GET 请求生成的验证码不同。

因此,-I 非常适合快速检查——该 URL 是否有效、重定向到哪里、文件大小是多少——但无法用于排查 GET 为何行为异常。 当你诊断一个真实的请求时,请使用真正的请求方法:

curl -sS -o /dev/null -D - https://example.com

该请求会执行一个正常的 GET 请求,将请求主体重定向至 /dev/null,并将请求头输出到标准输出。它能提供与 -I 类似的信息,同时不会改变请求本身。

如果你需要获取 POST 请求的特定请求头,同样的方法也适用:

curl -sS -o /dev/null -D - -X POST -H "Content-Type: application/json" \
     -d '{"a":1}' https://api.example.com/items

-D 和 -v:分离流与查看请求

-D 将头部写入到另一个目标位置。 手册中写道:“将接收到的协议头部写入到指定的文件中……若将文件名指定为‘-’(单个减号),则将其写入标准输出(stdout)。” 手册还指出,如果未接收到任何头部,该选项会“创建一个空文件”——这本身就是诊断信息。

curl -D headers.txt -o body.html https://example.com

这种干净的分离正是脚本编写所需要的。-D - 将头部发送到标准输出(stdout),而正文则发送到 -o 指定的位置,上述模式正是基于这种组合。

-v 同样会显示请求内容,而这往往正是你真正需要的部分。手册对前缀的解释非常明确:

详细输出行以字母为前缀:> 表示 curl 发送的头部,< 表示 curl 接收的头部,} 表示 curl 发送的数据,{ 表示 curl 接收的数据,* 表示 curl 提供的附加信息。

curl -v https://example.com 2>&1 | grep '^>'

这会准确显示 curl 实际传输的内容——而这通常与您的配置不符,因为库、默认设置和 .curlrc 文件都会添加或覆盖请求头。许多“服务器忽略了我的请求头”的问题都能在此得到解决。

请注意,详细输出会发送到 stderr,因此在进行管道传输前需要添加 2>&1。 这是有意为之:它能保持标准输出(stdout)中的主体内容干净。

手册还指出,自 curl 8.10 起,重复使用 -v 会提高跟踪级别。对于真正低级别的操作,--trace-ascii 会提供“所有入站和出站数据的完整跟踪转储,包括描述性信息”,同时省略十六进制内容以保持可读性。

手册中有一条值得重申的警告:跟踪和详细输出“可能包含敏感数据,包括用户名、凭据或机密数据内容。与他人共享跟踪日志时请务必注意并谨慎行事。”通过 URL 传递的代理凭据会出现在详细输出中。在将其粘贴到问题跟踪器之前,请先进行遮盖处理。

使用 ``%{header_json}`

` 获取机器可读的标头 这是大多数人从未见过的选项,于 curl 7.83.0 版本中添加,也是当你正准备针对标头文本编写正则表达式时的正确答案。

手册将其描述为“一个 JSON 对象,包含最近传输中的所有 HTTP 响应标头。 由于存在多个标头时可能有多个值,因此值以数组形式提供。”标头名称“以小写形式呈现,按在网络传输中出现的顺序列出”,重复的标头“按该标头首次出现的位置进行分组,每个值都呈现在 JSON 数组中”。

curl -s -o /dev/null -w '%{header_json}' https://example.com | jq
{
  "content-type": ["text/html; charset=UTF-8"],
  "cache-control": ["max-age=604800"],
  "set-cookie": ["a=1; Path=/", "b=2; Path=/"]
}

这同时解决了三个问题。名称被规范化为小写,因此无需进行不区分大小写的匹配。重复的标头(如 Set-Cookie )会以数组形式呈现,而不是被默默合并。而且输出内容无需编写解析器即可解析。

提取单个标头变得非常简单:

curl -s -o /dev/null -w '%{header_json}' "$URL" | jq -r '.["retry-after"][0] // "none"'

其他与之配合良好的 -w 变量:

curl -s -o /dev/null -w 'status=%{response_code} redirects=%{num_redirects} proxy=%{proxy_used} ip=%{remote_ip}\n' "$URL"

response_code 表示上次传输的状态,num_redirects 统计了跟随的重定向次数,redirect_url 显示当你未使用 -L 时,重定向本应跳转到的地址,remote_ip 是实际连接到的地址,proxy_used 若涉及代理则返回 1。当 NO_PROXY 模式可能已悄然将你的主机排除在外时,最后一个变量确实非常有用。

追踪重定向链

如果不使用 ``-L`

,curl 会在第一个重定向处停止,你只能看到该响应。 启用 -L`

后,curl 会显示链中 每个 响应的头部信息:

curl -sSL -o /dev/null -D - https://example.com
HTTP/2 301
location: https://www.example.com/

HTTP/2 200
content-type: text/html

每个代码块代表一次跳转。通过这种方式,你可以发现某个 URL 进行了三次重定向、某次跳转切换到了普通 HTTP,或者重定向过程中丢失了 Cookie。

有两种值得保留的输出模式:

curl -sSL -o /dev/null -w '%{num_redirects} hops -> %{url_effective}\n' "$URL"

计数和最终目标在一行中显示。 若想查看重定向的目标地址而无需实际跟随重定向路径:

curl -s -o /dev/null -w '%{redirect_url}\n' "$URL"

重定向链值得比人们通常所做的更频繁地检查。每次跳转都相当于一次往返,四跳的链条会带来实际的延迟,而通过不同主机的意外跳转,往往正是导致 Cookie 或 CORS 问题的原因。

通过代理的请求头

这里有两个需要补充的细节,初次接触时都容易让人感到困惑。

** 详细输出中会出现CONNECT 的响应。** 对于通过 HTTP 代理的 HTTPS 请求,curl 会先发出 CONNECT 请求来建立隧道,而该交互过程本身带有独立的请求头:

curl -v -x http://proxy.example.com:8080 https://example.com

你会看到 CONNECT ,接着是代理发出的 HTTP/1.1 200 Connection established ,然后才是真正的请求。第一个块是代理的响应,而非目标服务器的响应。将两者混淆是初学者常见的错误。当你只关心目标服务器的响应时,使用 --suppress-connect-headers 可以将其从输出中过滤掉。

%{http_connect} 会专门报告代理对 CONNECT 请求的响应代码,将其与目标服务器的状态区分开来。当出现故障且无法确定是哪个节点拒绝连接时,这种区分正是你所需要的:

curl -s -o /dev/null -x "$PROXY" \
  -w 'connect=%{http_connect} status=%{response_code} proxy=%{proxy_used}\n' \
  https://example.com

connect=200 status=403 表示代理正常工作,但目标服务器拒绝了连接。connect=407 表示代理要求凭据,但从未到达目标服务器。 这两种情况的解决方法截然不同,如果不作此区分,从应用程序的角度来看它们是完全相同的。

另请注意,对于通过隧道传输的 HTTPS 请求,代理无法添加或读取请求头——它只是中继加密后的数据字节。若在 HTTPS 响应中看到意外的请求头,它们来自目标服务器或其前端的 CDN,而非代理。

请求头实际上能告诉你什么

这一切的要点。仔细解读请求头,就能将猜测转化为诊断。

先看状态行。 200 请求成功。301/302 请求被重定向。403 请求被拒绝。429 请求受到速率限制。407 请求需要代理认证。502/503 上游出现故障。

Retry-After 会与 429 和 503 一起出现,并明确告知您需要等待多长时间。遵守该提示既正确,也是恢复正常服务的最快途径。若无视提示并立即重试,则会导致临时限制演变为更长时间的限制。

Content-Type 会告诉你实际接收到的内容。API 端点返回 text/html 意味着你收到的是错误页面而非 JSON,这正是导致大量解析失败的原因。

Content-Length 与实际接收内容对比。 声明长度很大而实际正文很短,说明数据被截断了。

缓存标头 — Cache-Control、ETag、Last-Modified — 用于判断是否可以避免重复获取。后续请求中出现 If-None-Match 和 If-Modified-Since 会将完整传输转换为 304,在带宽计费的情况下,这能直接节省成本。

Set-Cookie 显示服务器正在建立何种会话状态,而在预期应存在该标头却未出现的情况,往往能解释许多身份验证难题。

Server 和厂商特定头部 可识别源服务器前端存在哪些组件。如果响应中包含安全厂商的头部且带有 403 前缀,则表明阻断来自保护层而非应用程序——这属于不同类型的问题,应采取不同的应对措施。

非标准标头。 速率限制配额、请求标识符和 API 特定元数据通常以 x-为前缀的标头形式出现,它们往往是响应中最有用的信息。请求 ID 正是技术支持人员会询问的内容。

大家还问

如何使用 curl 查看响应头?

curl -i URL 会先显示响应头,然后显示正文。curl -D - URL 会将响应头单独写入标准输出。curl -v URL 会同时显示请求头和响应头。在调试真实请求时,请避免使用 -I,因为它发送的是 HEAD 请求而非 GET 请求。

curl 中 -i 和 -I 有什么区别?

-i 会将响应头与实际请求的正文一同显示。-I 则会发送 HEAD 请求,因此这是一个不同的请求,结果可能不同。当您需要获取正在调试的请求的头部信息时,请使用 -i 或 -o /dev/null -D -。

如何仅查看请求头而不显示请求正文?

curl -sS -o /dev/null -D - URL。该命令执行一个普通的 GET 请求,丢弃请求正文,并打印请求头。它能提供与 -I 类似的结果,同时无需更改 HTTP 方法——这一点很重要,因为某些服务器对 HEAD 请求的响应方式不同,甚至会直接拒绝该请求。

如何查看 curl 发送的请求头?

访问 curl -v URL,查找以 > 开头的行,这些就是 curl 发送的请求头。详细输出会发送到 stderr,因此若需过滤这些信息,请通过管道连接 2>&1。通过这种方式,您可以确认所配置的请求头是否确实已发送出去。

如何将 curl 头部以 JSON 格式获取?

curl -s -o /dev/null -w '%{header_json}' URL。该功能在 curl 7.83.0 中新增,它会将所有响应头部作为 JSON 对象输出,其中字段名均为小写,且值为数组,因此诸如 Set-Cookie 之类的重复头部会被保留,而非被合并。将其通过管道传递给 jq 可提取字段。

为什么使用代理时会看到两组头部?

对于通过 HTTP 代理进行的 HTTPS 请求,curl 会先发送 CONNECT 来建立隧道,而代理对此的响应会出现在目标服务器的响应之前。使用 --suppress-connect-headers 可隐藏该响应,或使用 %{http_connect} 将代理的状态码与目标服务器的状态码分开显示。

如何查看每次重定向的请求头?

添加 -L 使 curl 跟随重定向,并使用 -D - 或 -i —— curl 会打印链中每个响应的请求头,每跳一个代码块。%{num_redirects} 和 %{url_effective} 则会在单行中显示重定向次数和最终 URL。

响应头能否揭示我是否被封锁?

通常情况下,是的,而且比响应正文更可靠。带有 429 和 Retry-After 的响应表示速率限制。带有安全厂商标头且包含 403 的响应表示安全防护层。API 端点上带有 200 和 Content-Type: text/html 的响应则表示身份验证或登录页面。每种情况所需的解决方法各不相同,而只有标头能将它们区分开来。

总结

四种选项和一个常见的陷阱。-i 适用于日常检查;-D - 适用于需要将头部与正文分离的情况;-v 适用于需要同时查看发送内容和返回内容的情况;而 -I 仅适用于快速在线性检查——因为它发送的是 HEAD 请求,而服务器有权对 HEAD 请求和 GET 请求给出不同的响应。

如果你只能从本文中掌握一个技巧,那值得采用的是 %{header_json}。目前任何使用正则表达式解析标头文本的脚本都应改用此方法:使用小写名称、用数组表示重复出现的标头,并输出 jq 可读的格式。配合 %{response_code}、%{num_redirects} 和 %{proxy_used} 使用,它能将标头检查转变为可通过断言验证的过程,而非仅靠肉眼判断。

当请求出现问题时,在进行任何更改之前,请先阅读请求头。状态码、Retry-After、Content-Type 以及它们之间的任何供应商头部通常会直接指出问题所在——这比反复调整设置直到某项奏效要好得多,而且只需大约十秒钟。