Geonode logo
Geonode Team

Geonode Team

更新于:2026年10月7日

发布于:2026年9月2日

如何使用 curl 发送 HEAD 请求

`curl -I` 发送一个 HEAD 请求。`curl -X HEAD` 看起来应该执行同样的操作,但实际却会引发一个细微的错误,这足以让你在终端卡死的情况下白白浪费二十分钟。 curl 手册对此有明确说明,而理解其原因非常重要,因为这适用于你可能手动设置的每种方法。 本指南介绍了正确的语法、规范中关于服务器可省略内容的规则,以及 HEAD 请求能提供而 GET 请求无法提供的信息的情形。

我们为何关注此事:我们是 Geonode,主要销售代理服务,因此用户经常通过我们发送 HEAD 请求,以低成本检查链接、文件大小和可用性。 必须坦诚提醒的是:HEAD 是一种不同的请求,并非轻量级的 GET,将其等同于 GET 会导致确信无疑的错误结论。 某个 URL 对于 HEAD 请求返回 200 状态码,但对于 GET 请求可能返回 403 状态码。 一个在 HEAD 请求下不显示 Content-Length 的资源,在 GET 请求下可能会显示。而且,反机器人防护层可能会将异常的 HEAD 请求视为独立的信号。HEAD 非常适合其本来的用途;但若将其作为“如果我实际获取该资源会发生什么”的替代指标,则效果不佳。

正确方法

curl -I https://example.com

curl 手册中对 -I, --head

的说明如下:“(HTTP FTP FILE) 仅获取头部信息。 HTTP 服务器支持 HEAD 命令,本命令即利用该功能仅获取文档的头部信息。当用于 FTP 或 FILE URL 时,curl 仅显示文件大小和最后修改时间。”

输出:

HTTP/2 200
content-type: text/html; charset=UTF-8
content-length: 1256
last-modified: Thu, 17 Oct 2019 07:18:26 GMT
cache-control: max-age=604800

这就是标题问题完整的答案。以下内容则能帮你节省时间。

为什么 -X HEAD 是错误的

curl 手册 在“-X, --request”部分直接指出了这一点:

此选项仅更改 HTTP 请求中实际使用的字符串,不会改变 curl 的行为方式。例如,如果你想发出一个标准的 HEAD 请求,仅使用 -X HEAD 是不够的。你需要使用 --head 选项。

其工作原理如下:-X 仅交换方法字符串,除此之外不做任何更改。curl 仍会像执行 GET 请求一样运行,这意味着它仍然会期待收到响应正文。 服务器若正确实现了 HEAD 请求,则会发送请求头而不会返回正文。curl 会等待永远不会到达的内容,该命令似乎会卡住,直到超时或连接关闭才结束。

手册还警告了-X的另一种行为,这种行为会在重定向时让用户措手不及:“如果使用了--location,则通过--request设置的方法字符串将用于所有请求”。因此,-X POST -L会向重定向链中的每个跳点重新发送POST请求,这通常并非用户所期望的。

手册本身阐述了一般原则:“通常情况下,您不需要此选项。各类 GET、HEAD、POST 和 PUT 请求通常应通过专用命令行选项来调用。”请使用 -I 进行 HEAD 请求,-d 进行 POST 请求,-T 进行 PUT 请求,并将 -X 保留给真正不寻常的方法,例如 PROPFIND。

HEAD 方法的真实含义

RFC 9110 第 9.3.2 节用一句话对其进行了定义:

HEAD 方法与 GET 方法完全相同,唯一的区别在于服务器在响应中必须不得发送内容。

并阐述了其用途:“HEAD 用于获取所选表示形式的元数据,而无需传输其表示数据,通常用于测试超文本链接或查找最近的修改。”

这对服务器而言是一项严格的要求——MUST NOT 发送内容——这也是为什么当 curl 被要求接收内容时会卡住。

而关于标头的规则则故意制定得较为宽松,这也是人们常误解的地方:

服务器“应”在响应 HEAD 请求时发送与 GET 请求相同的标头字段。但是,对于那些仅在生成内容时才能确定值的标头字段,服务器“可”省略。

RFC 给出了一个具体示例:缓冲动态响应的服务器在处理 GET 请求时,可能会生成 Content-Length 和 Vary 这些“不会在 HEAD 响应中生成”的字段。 该文档将此类情况称为“轻微不一致”,并认为“这比为 HEAD 请求生成并丢弃内容更为可取,因为 HEAD 请求通常是为了提高效率而发出的。”

因此,HEAD请求中缺少 Content-Length 并不一定是个错误,也不一定具有特定含义。这可能仅仅是服务器拒绝计算某些内容,而这些内容只有在渲染页面时才能得知。

此外,若您正在开发相关工具,还有一条关于请求正文的规则值得了解。HEAD请求中的内容“没有普遍定义的语义,无法改变请求的含义或目标,并且可能会导致某些实现因其可能被用于请求走私攻击而拒绝该请求并关闭连接”。 RFC 规定,除非事先有明确约定,否则客户端“不应在 HEAD 请求中生成内容”。请勿在 HEAD 请求中发送请求体。

HEAD 的用途

真正有用的场景,这些场景都以传输全部数据为代价,只获取几百字节的信息。

检查 URL 是否有效:

curl -sI -o /dev/null -w '%{response_code}\n' https://example.com

在下载前查询文件大小:

curl -sI https://example.com/large.iso | grep -i content-length

追踪并报告重定向链:

curl -sIL -o /dev/null -w '%{num_redirects} hops -> %{url_effective}\n' https://example.com

检查服务器是否支持范围请求,这决定了中断的下载能否恢复:

curl -sI https://example.com/file.zip | grep -i accept-ranges

无需下载即可检查内容是否最新:

curl -sI https://example.com/data.json | grep -iE 'last-modified|etag'

批量链接检查,这是经典的应用场景,也是节省带宽效果最显著的地方:

while read -r url; do
  code=$(curl -sIL -o /dev/null -w '%{response_code}' --max-time 10 "$url")
  echo "$code $url"
done < urls.txt

在带宽限额环境下,这种节省效果尤为显著:原本每个URL需要传输500 KB数据的链接检查,现在只需传输几百字节。对于一万个URL而言,这意味着数据量从5 GB减少到仅几MB。

当 HEAD 令你误入歧途

以下是各种故障模式,这也是开头部分提出警示的原因。

服务器完全拒绝 HEAD 请求。 对于 GET 请求能正常工作的 URL,发送 HEAD 请求会导致 405 Method Not Allowed 错误。这种情况在静态内容中不常见,但在 API 和应用程序端点中并不罕见。

服务器对 HEAD 的处理方式不同。 状态码不同、头部信息不同,有时甚至在应用程序中采用完全不同的处理流程。RFC 允许省略基于内容的头部,而不同实现对这一规则的遵循程度各不相同。

缓存和 CDN 可能会对 HEAD 请求单独进行键值映射。 HEAD 请求的缓存标头可能指向与 GET 请求所击中的缓存条目不同的条目,因此 HEAD 并非检查缓存行为的可靠方法。

**反机器人系统会做出不同的响应。**来自未知客户端的 HEAD 请求本身就是一种信号,您收到的响应可能与浏览器发起的 GET 请求所获得的响应不同。

Content-Length 可能缺失或不正确。 这是规范允许的,在动态内容中很常见,但对于估算任何生成的内容的下载大小而言,这是一个不靠谱的依据。

重定向链可能不同。 某些服务器会将 GET 和 HEAD 重定向到不同的位置,特别是在涉及内容协商的情况下。

当你需要了解实际请求会产生什么结果时,请发送一个真实请求并忽略请求体:

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

这是一个真正的 GET 请求,请求头输出到标准输出,请求体被丢弃。你支付了带宽费用,便能获得准确的答案。请在两种方法之间审慎选择:若追求低成本,使用 -I ;若追求准确性,使用 -o /dev/null -D - 。

对于大型资源,还有一种折中方案——只请求一个字节,而不是整个资源:

curl -sS -r 0-0 -o /dev/null -D - https://example.com/large.iso

-r, --range 会检索“一个字节范围(即部分文档)”,因此 0-0 仅获取第一个字节。这是一个具有真实 GET 行为的真实 GET 请求,且几乎不消耗带宽。 手册中的注意事项:“许多 HTTP/1.1 服务器未启用此功能”,因此请先检查 Accept-Ranges: bytes ,若该功能缺失,则应预期会收到完整的响应。

值得复制到脚本中的模式

如果对上述命令进行一些结构化处理,它们的实用性会大大提高。

一个能如实报告结果的链接检查器。 简单的版本会将所有非200状态码都视为断链,这会在重定向情况以及服务器拒绝HEAD请求时产生误报。这个版本能区分这些情况:

check() {
  local url="$1" code
  code=$(curl -sIL -o /dev/null --max-time 10 -w '%{response_code}' "$url")
  case "$code" in
    200) echo "OK       $url" ;;
    405) code=$(curl -sSL -o /dev/null --max-time 10 -w '%{response_code}' "$url")
         echo "GET:$code $url" ;;
    000) echo "TIMEOUT  $url" ;;
    *)   echo "$code     $url" ;;
  esac
}

405分支至关重要:拒绝HEAD请求的服务器并不意味着链接失效,而通过GET请求重试是确认此情况的唯一方法。000是curl用于报告完全未收到HTTP响应的方式,这有助于区分网络故障与服务器错误。

并行处理,需谨慎。 链接检查具有明显的并行性,人们往往会忍不住想大规模运行。请克制:

xargs -P 8 -I{} sh -c 'check "$1"' _ {} < urls.txt

8 是一个合理的默认值。这里的上限取决于“轰炸”他人服务器时的礼貌程度,而非你自身的处理能力;如果链接检查器引发了速率限制事件,其造成的损失将远大于节省的资源。

务必设置超时。 针对无响应主机的 HEAD 请求会像 GET 请求一样挂起。使用 --max-time 10 并配合 --connect-timeout 5 可以限制超时,而在遍历数千个 URL 的循环中,正是这个限制确保了任务能够完成。

记录实际的 URL,而不仅仅是状态码。 在 -L 之后添加 %{url_effective},可以告诉你链接实际跳转到了哪里,这将“此链接有效”转变为“此链接有效且现在指向了其他地方”——通常这是更有价值的发现。

**缓存结果。**每次运行时重新检查所有 URL 既浪费带宽,也会耗尽用户耐心。请存储状态以及 ETag 或 Last-Modified,并在后续迭代中使用条件请求,这样对于未更改的资源,只需发送 304 请求,而非进行完整检查。

通过代理发送 HEAD 请求

有三点变化,都值得了解。

curl -I -x http://user:pass@proxy.example.com:9000 https://example.com

对于 HTTPS,CONNECT 会先被执行。 curl 会在发送 HEAD 请求前建立隧道,在详细输出中,代理对此的响应会出现在目标响应之前。--suppress-connect-headers 会隐藏这一响应;%{http_connect} 则会将代理的状态与目标的状态分开报告。

**关键在于节省带宽。**在按流量计费的网络环境下,一次 HEAD 请求仅消耗几百字节,而加载完整页面则消耗更多。对于大规模的链接验证、可用性监控和大小检查而言,这决定了任务是经济实惠还是成本高昂。 这是在按每千兆字节计费的模式下,为数不多的真正能带来显著优化效果的方案之一。

但阻断和验证机制的行为各不相同。 针对 GET 请求返回验证页面的反机器人防护层,可能会直接拒绝 HEAD 请求,反之亦然。 如果您使用 HEAD 请求来检查目标是否可达,请先对样本进行真实的 GET 请求以验证结果,再将其应用于整个列表。这就是我们在 为什么测试代理很重要 中提到的“静默失败”模式:请求成功,响应错误,却没有任何提示。

关于 curl 有一个值得了解的细节:-G, --get 与 --head 结合使用。手册中指出,当 -G 与 --head 结合使用时,“POST 数据会作为 HEAD 请求的 URL 参数附加”——这在需要将由键值对构成的查询参数用于 HEAD 请求时非常有用。

正确解读响应

充分利用返回的信息。

先看状态码。 200 存在。301 /302 已移动——请添加 -L 进行访问。403 被拒绝。404 已不存在。405 表示服务器不接受 HEAD 请求,请改用 GET 重试。429 表示请放慢请求速度。

**Content-Length **(若存在),需注意规范允许省略该字段。

**Accept-Ranges: bytes ** 表示支持可恢复下载和范围请求。

**Last-Modified 和 ETag ** 启用条件请求。-z 发送 If-Modified-Since —— 手册将其描述为请求“在给定时间之后被修改过的文件”——而 --etag-compare 处理 ETag 这一侧。 304 Not Modified 的成本几乎为零,是反复轮询资源的正确方式。

**Content-Type ** 会告诉你本应收到的内容。text/html 通常意味着出现错误或跳转至登录页面,而你原本期望的是 JSON 格式。

对于机器处理,请完全跳过文本解析:

curl -sI -o /dev/null -w '%{header_json}' https://example.com | jq

%{header_json} 会将所有响应头以 JSON 格式输出,其中名称为小写且值为数组,这样既能正确处理重复的响应头,又无需使用解析器。我们在 使用 curl 显示响应头 中介绍了该方法及其他检查选项。

大家还问

如何使用 curl 发送 HEAD 请求?

curl -I https://example.com. 完整形式为 --head。请勿使用 -X HEAD —— 手册中明确指出,该命令“不足以”发送正确的 HEAD 请求,因为它仅更改了方法字符串,而 curl 仍会期待收到响应正文。

为什么 curl -X HEAD 会卡住?

因为 -X 仅更改了请求行中的字符串,并未改变 curl 的行为。curl 仍然会等待响应正文,而服务器则正确地不发送任何内容,因为 RFC 9110 规定服务器在 HEAD 响应中“不得发送内容”。请改用 -I。

HEAD 和 GET 有什么区别?

HEAD 与 GET 完全相同,只是服务器不得发送响应正文。它的存在是为了在不传输内容的情况下获取元数据,通常用于链接检查或新鲜度测试。服务器应发送与 GET 请求相同的请求头,但可以省略那些仅在生成内容时才计算出来的请求头。

HEAD 是否总是返回与 GET 相同的请求头?

不。规范指出,服务器“应”发送相同的请求头,但“可”省略那些“仅在生成内容时才确定其值”的请求头——规范列举了 Content-Length 和 Vary 作为示例。规范认为,这些细微的不一致比生成并丢弃正文更为可取。

如何在不下载文件的情况下获取文件大小?

使用 curl -sI URL | grep -i content-length。请注意,对于动态生成的内容,该标头可能不存在,规范允许这种情况。若要获得几乎零开销且更可靠的答案,请使用 -r 0-0 请求一个字节,并读取 Content-Range 标头。

为什么某个 URL 在浏览器中可以正常访问,但在 curl -I 请求时却返回 405 错误?

因为服务器不接受该端点的 HEAD 请求。对于能够正常处理 GET 请求的服务器,405 Method Not Allowed 是对 HEAD 请求的有效响应。请尝试使用 GET 请求并忽略请求体:curl -sS -o /dev/null -D - URL。

我可以发送带有请求体的 HEAD 请求吗?

不建议这样做。 RFC 9110 指出,HEAD 请求中的内容“没有普遍定义的语义”,无法改变请求的含义,并且“可能会导致某些实现因其可能被用于请求走私攻击而拒绝该请求并关闭连接”。客户端不应在 HEAD 请求中生成内容。

HEAD 请求对检查代理是否正常工作有用吗?

部分有用。它能确认连接状态并以较低成本返回状态码,因此是一个不错的初步测试方法。但它无法告诉你真正的 GET 请求是否会成功,因为反机器人层通常会对这两种请求进行区别处理。在信任整个列表的 HEAD 结果之前,请先对样本使用真正的 GET 请求进行验证。

总结

只需两条命令就能完全实现这一功能。使用 curl -I URL 可发送标准的 HEAD 请求,而当需要获取真实 GET 请求会产生的请求头时,则使用 curl -sS -o /dev/null -D - URL。但 -X HEAD 无法正常工作,手册对此也有明确说明:它只是更改了命令名称,并未改变其行为,因此 curl 会等待服务器发送本不该发送的请求体。

关键在于你需要哪一种。HEAD 的资源消耗要低得多——仅需几百字节,而 GET 请求则需要整页内容——这使其成为链接检查、可用性监控和大小估算的理想工具,无论请求量大小,尤其是在带宽受限的情况下。 但它并不适合预测实际请求会返回什么结果,因为服务器被允许省略基于内容的标头,可能会直接拒绝 HEAD 请求,并且通常会通过不同的逻辑来处理它。

如果你需要 GET 的准确性,但又不想消耗带宽,-r 0-0 是一个被低估的中间方案:这是一个真正的 GET 请求,但只获取一个字节。建议先检查 Accept-Ranges: bytes,因为许多服务器无论如何都会返回整个文件。