HEAD请求实测124个站:九成的站和GET给的不是同一套头

HEAD请求实测124个站:九成的站和GET给的不是同一套头
张文保 32 分钟阅读 2,169 阅读
本文目录
  1. 同一个地址,换一种问法,答案会不会变?
  2. 为什么值得花时间量这个
  3. 先交代一个负面结果
  4. 换成OPTIONS,124个站为什么给出9种答案?
  5. 这些答案是随手给的,不是设计出来的
  6. 规范要求的那个Allow头,有几个站给了?
  7. 问它支持哪些方法,它为什么把整页发了过来?
  8. 这件事花的是谁的钱
  9. 同一个页面,HEAD为什么说它0字节?
  10. 为什么会是0,我去挖了一层
  11. 反方向那14个站
  12. 两种问法拿到的响应头,是同一套吗?
  13. 差异不是均匀分布的
  14. 我原本担心的那件事没发生
  15. ETag那一格,我差点写出一个错结论
  16. 那两个真的方法差异长什么样
  17. 那32个每次都变的,其实是更大的问题
  18. 同一个站的首页和内页,答案为什么不一样?
  19. 第三方脚本那一层也一样
  20. robots.txt用HEAD问,会得到什么?
  21. weber.com的robots.txt,两种问法连压缩算法都不一样
  22. 这一轮,我的尺子错了五次
  23. 这一轮没测到的
  24. 顺带说说那7个真进不去的
  25. 自己的站该怎么测?
  26. 第一步,三种问法各来一次
  27. 第二步,把ETag连问两次
  28. 第三步,别忘了robots.txt和sitemap
  29. 第四步,看看你的前面站着谁
  30. 常见问题解答
  31. Googlebot会用HEAD请求抓我的站吗?
  32. OPTIONS返回404,需要修吗?
  33. HEAD把Content-Length报成0,会不会影响SEO?
  34. ETag每次都变,具体怎么修?
  35. 我的站robots.txt用HEAD也是404,怎么办?
  36. 这套测法值得做成定期检查吗?
  37. 权威参考资料
摘要:对131个英文大牌站点的同一个首页地址,分别用GET、HEAD、OPTIONS问了一遍。能问通的124个站里,112个站两种问法拿到的响应头不是同一套,占90.3%;OPTIONS这个问题收到了9种不同答案;50个站在HEAD上把内容长度报成0,而GET说有12万字节;还有一个站的robots.txt,用GET拿是200,用HEAD拿是404,复验三次都一样。

做站的人测页面,测的几乎永远是GET。浏览器发的是GET,你自己打开页面发的是GET,性能工具跑的也是GET。

但每天来敲门的机器不止用这一种问法。链接检查工具为了省流量优先发HEAD,CDN预热和缓存刷新用HEAD探活,跨域请求前浏览器会先发一个OPTIONS问一句你允许我干什么,监控脚本更是清一色HEAD。

这些通道你没测过。上一轮量证书那本公开账本时有个感受:一个系统总有几面是别人在看、你自己没看过的。这一轮把这句话挪到HTTP这一层,做法很朴素——同一个地址,换三种问法,看答案会不会变。

同一个地址,换一种问法,答案会不会变?

先把测法说清楚。131个站的首页,依次发GET、HEAD、OPTIONS三种请求,记录状态码、全部响应头、正文字节数和耗时。之后再往下测一层内页,以及robots.txt。

三种方法在标准里的分工是明确的。按RFC 9110对HTTP语义的定义,HEAD的语义是与GET完全相同、只是不返回正文,服务器发的响应头应当和GET一致;OPTIONS用来询问某个资源支持哪些方法,服务器应当在Allow头里给出答案。

方法标准里的分工谁在用它
GET取回完整内容浏览器、搜索引擎抓取、你自己
HEAD只要元信息,不要正文链接检查器、CDN预热、可用性监控、部分AI爬虫
OPTIONS问这个资源支持哪些方法浏览器的跨域预检、接口调试工具

说白了:HEAD应该是GET摘掉正文,OPTIONS应该是一张能力清单。标准写得很清楚,实现起来是另一回事。

为什么值得花时间量这个

因为用HEAD的那批客户端,恰恰是决定你在别人系统里长什么样的那批。外链监控用HEAD判断你的页面还在不在,判错了就是一条外链被标成失效;社交平台抓预览卡片时先探一下类型和大小;死链批量排查工具更是几乎全靠HEAD,否则扫一遍全站的流量成本没法接受。

MDN对HEAD方法的说明里有一句实现建议:HEAD响应里的头应当和同一个请求用GET时一致,否则会误导缓存与工具。这句话本轮成了很好的对照基线——它说的正是那90.3%没做到的事。

先交代一个负面结果

我原本最怀疑的是状态码:会不会有站GET给200、HEAD给别的?

没有。124个能问通的站,首页的HEAD状态码全部是200,一个例外都没有。这一格干干净净。

负面结果也是结果,而且它把范围收窄了:这件事的差异不在状态码上,在状态码后面那一堆东西上。往下每一节都是从这里开始的。

换成OPTIONS,124个站为什么给出9种答案?

同一个问题——这个地址支持哪些方法——124个站给了9种不同的回答:

OPTIONS返回站数占比字面意思
4046250.0%这个地址不存在
2002721.8%成功
4051612.9%方法不允许
40054.0%你的请求有问题
50043.2%服务器出错了
40343.2%拒绝
20443.2%成功,没有内容
50110.8%没实现这个方法
41010.8%已永久删除

这九种回答里,语义上说得过去的只有三种:405说我不支持这个方法,501说我没实现这个方法,200或204带着Allow头给出清单。

占了一半的那个404是什么意思?它说的是这个地址不存在。可同一个地址,你用GET问它,它返回的是一个完整的首页。

500更离谱,服务器自己崩了。anker.com和soundcore.com就是这样,OPTIONS一发过去先来个500,倒是很讲礼貌地在500的响应里附上了Allow: GET, HEAD——出错了还不忘告诉你它支持什么,大概是本轮最有礼貌的一次崩溃。

410的意思是这个资源永久没了。一个每天在卖货的首页,被问到支持哪些方法时,回答说自己已经永久删除。

那4个返回204的站是唯一一批把这件事做对的:204的意思是我收到了、处理完了、没有正文要给你,正好对应OPTIONS这个问题的性质。它们的响应通常很小,几百字节,带着允许的方法与头部就结束了。整批131个站里,只有这几个站的这条路径像是有人专门写过。

这些答案是随手给的,不是设计出来的

看到这个分布,第一反应可能是各家技术选型不同。往下拆一层会发现不是这么回事:回答的形态和平台高度相关,跟品牌无关。大量Shopify站给的是404,一批Next.js站给的是405或者500,Salesforce Commerce那批给的是200。

换句话说,这九种答案里绝大多数不是任何人做过决定的结果,是各家框架对一个没人处理的方法给出的兜底行为。这和响应头里那248个死字段是同一类现象:不是写错了,是根本没人写过,默认值替你回答了。

规范要求的那个Allow头,有几个站给了?

RFC 9110写得很明确:服务器返回405的时候,必须生成一个Allow头,列出这个资源支持的方法。这是必须,不是建议。

本轮OPTIONS返回405或501的站一共17个。带了Allow头的,3个。

  • canyon.com:GET, POST, HEAD
  • eufy.com:GET, HEAD
  • vuoriclothing.com:GET, HEAD

剩下14个站告诉你这个方法不允许,却不肯说哪些方法才允许。这就像敲门问能不能进,里面回一句不行,然后没了下文。

反过来,有5个站在不必给的地方给了:assos.com、braun.com、uniqlo.com在200的响应里带了Allow,anker.com和soundcore.com在500里带了。该给的时候14个站不给,不必给的时候5个站给了,这一格的实现基本处于随机状态。

这条规则的实际影响不大,因为几乎没有客户端真的依赖Allow头做决策。但它是一个很好的指示器:一个连硬性要求都没实现的代码路径,说明这条路径从来没被人认真看过。后面几节的问题都长在同一条路径上。

问它支持哪些方法,它为什么把整页发了过来?

OPTIONS是问问题,不是要内容。但124个站里,有40个站的OPTIONS响应带回了超过1KB的正文,占32.3%。其中22个站带回的字节数与GET几乎一模一样——它把整个首页又发了一遍。

站点OPTIONS状态OPTIONS正文GET正文
casetify.com2001461241字节1461238字节
shein.com20011376941137734
mejuri.com200928469928116
cybex-online.com200791454791454
philips.com200638650638650
on.com200610431610431
arcteryx.com200509224509224
charleskeith.com404512752571774

casetify那一行值得停一下:1.46MB。一个只是想知道你支持什么方法的客户端,收到了一份完整的商品首页。为了确认这不是偶发,我对其中五个站各测了两次,两次的字节数差在个位数以内,差值来自页面上的动态内容——这个行为完全稳定。

charleskeith那一行更有意思:状态码说404找不到,正文里却塞了512KB的HTML。这是典型的软404,只不过发生在一个很少有人看的方法上。Nginx配置每次reload都通过、Googlebot却撞在301上那一轮讲的是同一类事:配置本身没报错,行为在另一条路径上跑偏了。

这件事花的是谁的钱

浏览器发跨域预检时用的就是OPTIONS。MDN对OPTIONS的说明里给的标准回法是返回204、带上允许的方法与头部,不带正文。

一个站如果对每个OPTIONS都回一整页,那么页面上每一个触发预检的跨域字体、接口、埋点调用,都会在真正的请求之前先拖回来一份首页。

算一笔账:casetify的首页1.46MB,如果页面上有三个跨域资源触发预检,光预检就多出4.4MB的回源流量,而这些字节客户端一个都用不上,收下就扔。对TTFB和多层缓存那本账来说,这属于纯粹的浪费——不影响正确性,只影响账单和延迟。

还有个更隐蔽的代价:预检响应本身是可以被缓存的,但前提是服务端给出相应的头。回一整页HTML的那些站,多半也没给预检缓存时间,于是每一次跨域调用之前都要重来一遍。

同一个页面,HEAD为什么说它0字节?

这是本轮最整齐的一组数字。124个可测的站里,把GET和HEAD的Content-Length摆在一起:

对账结果站数
两边一致60
HEAD报0,GET报真实长度50
GET不给(分块传输),HEAD反而给了14
两边都给但数值不同0

那50个站的形态完全一致:GET说这个页面压缩后8万到34万字节不等,HEAD说0。casper.com的GET报99951、HEAD报0;brooklinen.com的GET报122889、HEAD报0;dreametech.com的GET报342956,HEAD照样是0。

这50个站里,49个是Shopify,剩下那1个weber.com是Salesforce Commerce。又是一个平台级的行为,不是50个团队各自的疏忽。

为什么会是0,我去挖了一层

数字太整齐,得知道机制才敢写。做了一组对照实验:同一个地址,分别带上和不带压缩协商各发一次。

请求头GET的Content-LengthHEAD的Content-Length
accept-encoding: gzip, deflate, br99951(声明br压缩)0(同样声明br)
accept-encoding: identity不给不给

答案出来了:这个0只在压缩协商成功时出现。服务端的压缩环节算的是它实际写出去的字节数,而HEAD响应的正文被丢弃了,压缩器写出0字节,于是Content-Length如实写了0——同时还留着一个content-encoding: br,声明说我压缩过了,长度是0。

逻辑上自洽,语义上是错的。RFC 9110说HEAD响应里的Content-Length应当指示GET会返回多大,而不是这一次实际发出去了多少。

后果落在谁身上?落在所有拿HEAD探大小的工具身上:链接检查器会认为这个页面是空的,CDN的预热逻辑可能因此跳过它,任何按体积做阈值判断的监控都会读到0。如果你的工具链里有一条“正文为空则告警”的规则,这50个站会集体误报,而它们其实好好的。

还有一层值得想想:这50个站占了可测样本的四成。任何一份声称覆盖了主流独立站的自动化报告,只要它某一步用了HEAD探体积,就有四成的样本读到的是0。这个错误不会报错、不会超时、不会留下任何异常痕迹,它只是安静地把一个正常页面标成空的。最难查的问题从来不是崩溃,是一个语法正确、语义错误、还每次都稳定复现的答案。

反方向那14个站

另一头也有反常的:14个站的GET用分块传输、不给Content-Length,HEAD反而给了一个具体数字。

站点GET的Content-LengthHEAD的Content-Length
kotn.com不给(分块)1527015
traeger.com不给(分块)789228
article.com不给(分块)655021
anker.com不给(分块)316119
shein.com / swarovski.com等6个不给(分块)0

前面几个给的数字看着像页面的真实大小,可GET那一次并没有承诺过任何长度。后面6个更绕:GET拒绝回答,HEAD回答了一个错的。

同一个资源,两种问法,一个说不知道,一个说是零。两句话都不对,而它们出自同一台服务器。

两种问法拿到的响应头,是同一套吗?

不是。这是本轮覆盖面最广的一个结论:124个站里,112个站的GET和HEAD响应头条数不相同,占90.3%。

站点GET响应头条数HEAD响应头条数
farfetch.com4936
bellroy.com3627
quince.com3628
weber.com3424
uniqlo.com2516

把具体丢了哪些字段拆开看:

字段GET上有的站数HEAD上丢了
set-cookie9711
content-encoding12310
vary1136
cache-control621
link621
last-modified111
content-type1240
etag660

最值得注意的是vary那一行。6个站在HEAD响应里没有vary,GET上有。vary决定了缓存该按哪些请求头分片存储,缺了它,中间层就会把本该分开存的几个版本混成一个。决定收录哪一版页面的往往不是你的配置那一轮讲的正是这件事,只不过那次的分岔来自请求头,这次的分岔来自请求方法。

set-cookie丢了11个站,这个反倒可能是好事——HEAD探测本来就不该拿到会话Cookie。但它同样说明一件事:这两条响应路径在服务端是分开走的,不是同一份头减掉正文。

content-encoding丢了10个站也属于同一类:GET声明我压缩了,HEAD什么也不说。对客户端来说,这两次收到的是关于同一个资源的两份互相矛盾的描述。

为什么会分开走?常见的原因有三种:一是应用框架对HEAD做了短路处理,在生成完整响应之前就提前返回;二是反向代理或CDN在边缘层拦下了HEAD,用一份简化的头直接回;三是某些中间件只在有正文时才追加自己那几条头。三种情况的共同点是,写这段逻辑的人当时想的是省资源,没想过有人会拿这两份头做对比。

差异不是均匀分布的

把差值排一下会发现,条数差得最多的站,差的往往是同一批东西:CDN加的缓存状态头、安全相关的自定义头、以及一整组追踪用的内部标识。响应头自相矛盾那一轮量的是同一批头里前后打架的情况,这一轮是同一批头在两种方法上有和无的差别。

换个角度看,这也解释了为什么这类问题长期没人发现:缺的那些头,绝大多数不影响页面显示。它们只影响缓存、监控和自动化工具,而这三样东西出问题时,通常不会有人来提工单。

我原本担心的那件事没发生

动手之前我最担心的是X-Robots-Tag:假如一个页面靠响应头声明noindex,而这条头只在GET上出现,那么用HEAD做审计的工具就会漏判,页面被判成可索引,实际上不是。

实测结果是这个风险在这批站上不成立——124个站里只有1个站在首页发了X-Robots-Tag,而那一个GET和HEAD都发了。样本太小,不足以下任何结论。写在这里是因为它值得你在自己站上验一遍:如果你的noindex是靠响应头下发的,那这条头在HEAD上必须也在。

ETag那一格,我差点写出一个错结论

这是本轮最该记下来的一次翻车,因为它差一点就变成一个很吸引人、但完全错误的标题。

首轮数据是这样的:66个站的GET给了ETag,其中41个站的HEAD给的ETag和GET不一样。62%的不一致率,看起来是个大发现——同一个地址两种问法给出两个不同的版本凭据,条件请求当场失效。

写之前我留了个心眼,做了一组对照:如果连发两次GET,ETag会不会也变?

会。而且是大面积地变。把41个站逐个复测,每站各发两次GET、两次HEAD:

复测结论站数
每次请求都在变,跟用什么方法无关32
本次复测两边一致,首轮那次是抖动7
GET自身稳定、HEAD给的确实不同(真的方法差异)2

41变成了2。原来那个62%的不一致率,量到的根本不是方法差异,是这批站的ETag本身每次请求都在换。

那两个真的方法差异长什么样

kotn.com是最干净的一个样本,它的ETag在两种方法下都稳定,但形态不同:

GET  → W/"174ce7-wIoHQXl4rJjaJ5AS5byKwUi/PGU"
HEAD →   "174ce7-wIoHQXl4rJjaJ5AS5byKwUi/PGU"

引号里的值一个字符都不差,区别是GET那个前面多了个W/。这个前缀在标准里有确切含义:带它的叫弱验证器,只保证语义等价;不带的叫强验证器,保证字节完全相同。同一份内容,用GET问它说大概是这个,用HEAD问它说就是这个。

强弱之分不是文字游戏,它决定了这个凭据能不能用于范围请求这类要求字节精确的场景。一个资源在两种方法下给出不同强度的承诺,等于把判断责任推给了客户端。

那32个每次都变的,其实是更大的问题

把错误结论修掉之后,剩下的事实反而更糟糕:32个站的ETag每次请求都在变。这意味着任何客户端拿着上一次的ETag来问“还是这个版本吗”,答案永远是否定的,条件请求在这些站上等于不存在。

之前量过142个站里只有52个能回出304,当时的结论停在回不出。这一轮补上了其中一类原因:不是它不肯回304,是它每次给的凭据都不一样,客户端拿什么来问都对不上。

对抓取预算的影响是实打实的,每一次重访都得整页重下,抓取预算优化里省下来的那点额度,在这一格上全漏光了。要修的话,第一步是定位那个每次都变的因子——多数情况下是页面缓存键里掺进了时间戳或者分流标记,具体的排查口径见浏览器缓存头怎么配那一篇。

同一个站的首页和内页,答案为什么不一样?

前面所有数字都来自首页。往下测一层内页之后,出现了本轮最反直觉的一组数据。

能同时测到首页和内页的站有109个,其中68个站的首页和内页在OPTIONS上给出了不同的答案,占62.4%。而且方向高度集中:

首页 → 内页站数
404 → 20041
200 → 4044
200 → 4053
405 → 2003
其余组合17

41个站在首页说这个地址不存在,在内页说完全支持。同一个域名,同一套代码,隔着一个路径的差别,能力清单就反过来了。

原因不复杂:根路径和商品路径在框架里往往由不同的路由规则接管,一个有兜底、一个没有。但它带来的结论很硬:用首页测出来的HTTP行为,不能代表这个站。任何一份只测首页的审计报告,在这一格上有62%的概率给出与内页相反的描述。

这条经验对做站点审计的人特别值钱:抽样的时候,首页从来不是一个有代表性的样本,它是全站最特殊的那一页——模板不同、缓存策略不同、路由优先级也不同。

为什么404到200这个方向占了绝对多数?一个合理的解释是路由的匹配顺序:根路径通常由一条最宽泛的规则兜底,而这条兜底规则往往只声明了GET;商品页、分类页那些带参数的路径,反而走的是更完整的处理链,方法限制没那么死。越是重要的入口,越可能落在最粗糙的那条规则上——这个反差在很多站的配置里都成立,不只是HTTP方法这一格。

第三方脚本那一层也一样

顺手测了每个站首页上引用的第一个静态资源。79个能取到的资源里,有两个站命中了同一个第三方地址:cdn.cookielaw.org上的同意管理脚本,用GET取是200,用HEAD取是405。

这是一个装在无数站点上的合规脚本。它自己不允许HEAD,意味着任何一个想检查这个脚本还在不在的自动化流程,都会拿到一个方法不允许的答案。第三方脚本会悄悄拉进一串你看不见的域名,而这些域名的行为完全不由你决定,这里又多了一个例子。

robots.txt用HEAD问,会得到什么?

最后一层测的是robots.txt。选它是因为它的读者几乎全是机器,而机器里有一部分习惯先用HEAD探一下。

这一层能拿到200的站有62个,比首页少,原因下一节说。62个站里61个的HEAD也是200,只有一个例外:

sostrenegrene.com的robots.txt,用GET拿是200、312字节的纯文本;用HEAD拿是404。

为了确认不是抖动,交替测了三个来回,六次请求:GET三次全是200,HEAD三次全是404。完全稳定。

这意味着什么,取决于谁来问。一个先用HEAD确认文件存不存在、存在再GET取内容的爬虫,在这个站上会得到没有robots.txt这个结论。而没有robots.txt的默认含义是全站可抓。这个站robots.txt里写了什么,对这类客户端来说完全不生效——不是被忽略,是压根没被发现。

之前量过robots.txt里35.7%的指令根本没有接收方,也量过分组不继承导致的规则落空。那两轮讲的是写了没人读、写了读错组。这一次是第三种:写了,文件也在,但换个问法就查无此文。

weber.com的robots.txt,两种问法连压缩算法都不一样

还有一个稳定复现的:

方法Content-LengthContent-Encoding实收字节
GET770br2289
HEAD0gzip0

长度报0已经见过了,这次连声明的压缩算法都换了一个——GET说用的是br,HEAD说是gzip。两次复测结果完全一样。这说明两条路径上接手压缩的根本不是同一个环节。

robots.txt这一层还有个顺带的数字:61个站里,有41个的GET和HEAD响应头条数不同,占67.2%。连这么一个纯文本小文件,两种问法拿到的元信息都不是同一套。另外还有4个站的robots.txt直接返回404——这个数字本身不算问题,没有robots.txt是合法状态,但它提醒一件事:这个文件的存在与否,值得纳入常规巡检。

这一轮,我的尺子错了五次

上面每个数字背后都有一次判据修正。五条里有三条改变了结论方向,如果不查,这篇文章会有三个错误的核心数字。

错在哪差点写出的结论改法
ETag只比了一次,没验证它自身稳不稳41个站HEAD与GET的ETag不同,62%的站条件请求失效每站补发GET两次,41个里32个是每次都变、7个是抖动,真方法差异只有2个
把一次抽样当成站点属性47个站拒绝机器访问,可测样本只有84个打乱顺序多轮重试,第一轮就救回39个,最终只剩7个真进不去,分母从84变成124
内页选择器没排除字体和JS6个站内页GET通、HEAD返回404逐条看URL,其中4条抽到的是woff2、ttf、js文件,不是内页。归到资源层重新算
拿解压后的字节数去比Content-Length几乎每个站的长度都对不上,全站都在乱报HTTP客户端会自动解压,Content-Length说的是压缩后的长度。只能头对头比
没验证HEAD报0的成因就下判断这50个站的HEAD实现是坏的用identity再测一遍,发现两边都不给长度。这个0是压缩路径的产物,不是随手写的

第三条那次没有改变结论,但改变了归类。首轮统计说6个站内页GET通、HEAD返回404,看着像是内页层的问题;逐条打开URL才发现,其中4条抓到的根本不是内页,是页面上引用的woff2字体、ttf字体和一个js文件——我的内页选择器排除了css和js的常见后缀,却漏掉了字体格式,还有一个兜底分支根本没做过滤。把这4条归回资源层之后,内页层真正的问题只剩下访问被拦这一种。判据写错不一定会让数字变大或变小,它可能只是把问题挂到了错误的墙上。

第一条最值得说。一个62%的不一致率,看起来像个很有冲击力的发现,实际上量的是另一件事。判断的方法其实很朴素:任何一个“A和B不一样”的结论,都要先问一句“A和A自己一样吗”。这个对照只花了几分钟,省下的是一篇建立在错误前提上的文章。

第二条也一样。第一轮采集时47个站返回429或403,我差点写成这些站拒绝机器访问;换个时间、打乱顺序重试一轮,39个立刻就通了。被拦不是站点的属性,是那一次请求的结果。这条经验对所有做批量采集的人都成立:单次失败率不能当成覆盖率来报。

这一轮没测到的

把局限说在前面,免得这些数字被当成比它本身更大的结论。每个站只测了首页、一个内页和一个静态资源,抽样很薄;内页是从首页HTML里按规则挑的第一个符合条件的链接,不是按模板均匀抽的;所有请求都从同一个出口发出,没有换过地区,而不少站的边缘规则是按地区分的。

另外,POST、PUT、DELETE这几个方法一个都没测——不是忘了,是不该测。对别人的生产站发这类请求可能真的改动数据,这条线不能碰。所以本文所有结论都只覆盖只读方法,一个站在写方法上是什么表现,本轮不知道,也不打算知道。

顺带说说那7个真进不去的

三轮重试之后,稳定进不去的还有7个:aboutyou、fjallraven、pandora、thefarmersdog、warbyparker返回403,govee返回400,underarmour返回418。拦截方几乎全是Cloudflare。

其中429这个状态码值得单独提一句。它的字面意思是请求太多、稍后再试,可我这边第一个请求就收到了它。Google对抓取相关状态码的说明里,429被归为要求放慢的信号,抓取端收到之后会降低频率。用一个表示慢一点的状态码来表达不欢迎你,这两件事对机器来说完全不同:收到429的客户端会退避、会重试,而收到403的会记下来不再来。

之前量过robots.txt说允许、GPTBot仍然进不去,讲的是同一件事的另一面——身份识别这一层的决定,往往比robots.txt里写了什么更能决定谁能进来。

自己的站该怎么测?

不需要工具,curl就够。要点是每一条都测两个地址:首页和一个真实内页。

第一步,三种问法各来一次

curl -sI https://example.com/
curl -s -o /dev/null -D - https://example.com/
curl -s -X OPTIONS -D - -o /dev/null -w 'body=%{size_download}\n' https://example.com/

把三份输出并排放着看,重点盯四样:状态码是否一致、Content-Length是否一致、vary和cache-control在不在、OPTIONS有没有回正文。最后一项最容易被忽略,因为curl默认不显示正文大小,上面那个size_download就是为它加的。

第二步,把ETag连问两次

for i in 1 2 3; do curl -sI https://example.com/ | grep -i '^etag'; done

三次的输出如果不一样,那么你的条件请求是失效的,跟方法无关。这一格上如果发现问题,优先级要排在最前面——它直接影响回访抓取的成本,比本文其他任何一条都实在。

第三步,别忘了robots.txt和sitemap

curl -sI https://example.com/robots.txt
curl -sI https://example.com/sitemap.xml

这两个文件的读者全是机器。HEAD返回的状态码必须和GET一致,长度也不该是0。sitemap里那个lastmod值不值得信是另一个话题,但前提是这份文件得先能被发现。

第四步,看看你的前面站着谁

如果你的站在CDN或者WAF后面,上面这些行为有一部分不是你的应用决定的。测的时候顺手记下server头和CDN的缓存状态头,出现异常时先判断是哪一层的行为。本文里那50个报0长度的站,没有一个是自己写的代码造成的。

症状大概率在哪一层要不要管
HEAD的Content-Length是0平台或压缩环节自己改不了,但要知道监控会读到0
OPTIONS返回整页应用路由缺兜底该管,白花流量
405不带Allow应用影响小,顺手补
ETag每次都变应用或页面缓存键优先处理
robots.txt的HEAD是404路由或边缘规则该管,影响抓取
首页与内页答案不同路由规则不一致知道就行,别用首页代表全站

这一轮跑下来,保哥最想留下的不是某个具体数字,而是一个测法上的习惯:你以为你在测一个站,其实你只是在测它对你那一种问法的反应。换个方法、换个路径、换个时间再问一次,成本很低,而它揭示的东西往往和第一次完全不同。

常见问题解答

Googlebot会用HEAD请求抓我的站吗?

Google的抓取以GET为主,官方文档讨论抓取行为时也是按GET来描述的,所以本文这些差异对自然搜索的直接影响有限。真正会踩到的是另一批客户端:链接检查工具、CDN预热、外链监控、社交平台的预览抓取,以及相当一部分AI爬虫和第三方审计工具。它们用HEAD是为了省流量,代价是可能拿到一份和GET不一样的答案。

OPTIONS返回404,需要修吗?

如果你的页面上没有任何跨域请求,影响接近于零,可以放着。有跨域资源就值得看一眼:浏览器发预检时用的就是OPTIONS,返回一个带Allow的204是标准做法,返回整页是纯浪费。判断依据不是状态码好不好看,是它有没有把正文一起发出来。

HEAD把Content-Length报成0,会不会影响SEO?

对搜索引擎的收录判断没有直接影响,因为它们取内容用的是GET。会受影响的是你自己那套工具链:按体积做阈值告警的监控会读到0,链接检查器可能把页面判成空,CDN的某些预热策略会跳过零长度资源。如果这些工具在你的流程里有决策权,就得知道这个数字不可信。

ETag每次都变,具体怎么修?

先定位它是谁生成的。如果是应用层按响应内容算哈希,要检查内容里是不是掺了每次都不同的东西,比如时间戳、随机的追踪标识、A/B分流标记。如果是平台或CDN生成的,就看缓存键的构成——它把某个每次都变的因素算了进去。用SaaS建站平台的话,这一格多半改不了,但至少要知道自己的回访抓取成本是按整页重下算的。

我的站robots.txt用HEAD也是404,怎么办?

先确认是哪一层返回的:直接请求源站再测一次,如果源站正常,问题在CDN或边缘规则上,通常是某条路由规则只匹配了GET。如果源站也是404,那是应用路由的问题。这一条建议排在前面处理,因为robots.txt是少数几个纯粹面向机器、又直接影响抓取范围的文件。

这套测法值得做成定期检查吗?

值得,但频率不用高。这类行为在平台升级、CDN换配置、路由规则调整之后才会变,平时相当稳定。建议是在每次基础设施变更之后跑一遍,十几条curl命令的事。真正需要长期盯的只有ETag那一格,因为它和抓取成本直接挂钩。

权威参考资料

分享到
标签
版权声明

本文标题:《HEAD请求实测124个站:九成的站和GET给的不是同一套头》

本文链接:https://zhangwenbao.com/head-options-method-response-divergence-audit.html

版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交