Range请求回了200,里面却连全文的千分之一都不到
本文目录
- 一个标着200的响应,正文只有全文的千分之几?
- 先看最离谱的那两个站
- 规范其实把话说死了
- 这不是我的客户端搞错了
- Accept-Ranges这句声明,有多少是真的?
- 先看有多少站愿意开口
- 为什么“沉默”这一格这么大
- 声明是源站写的,实做是边缘做的
- 同一个地址,只换一个Accept-Encoding,为什么答案不一样?
- 6个站在两遍之间翻了面
- 在自己的nginx上,这件事能一个开关一个开关地复现
- Content-Range后面那个分母,量的是压缩前还是压缩后?
- 要末尾一百字节、要两段、要一段不存在的,分别会怎样?
- 三种写法,三种冷淡
- 越界那一问最值得看
- 静态资源那一侧,为什么反而正常?
- 同一个请求头,两种命运
- CSS比JS高出来的那几个百分点
- 顺手撞到的一个口径问题
- 按平台看,这件事几乎是被几家决定的
- 爬虫会不会真的发Range?这件事什么时候伤到收录?
- 先说不会疼的部分
- 再说真会疼的三种场景
- 还有一层容易被忽略的:CDN自己在用
- 我这套量法在哪几处会骗人?
- 四个已经踩实的坑
- 三处说在前头的边界
- 自己站上怎么查、怎么配
- 三条命令看清现状
- 该配什么,不该配什么
- 一个真实的排查
- 常见问题解答
- 首页不支持范围请求,会影响SEO吗?
- 为什么我开了gzip之后Accept-Ranges就没了?
- 状态码200却带着Content-Range,客户端会怎么处理?
- Content-Range里的总长度能当文件大小用吗?
- 越界的Range应该返回什么?
- 一次要两段的多段范围,值得支持吗?
- 视频文件一定要支持范围请求吗?
- CDN会不会替我把这件事解决了?
- 这批数字能代表我的站吗?
- 权威参考资料
摘要:给107个海外品牌站的首页各多发一行Range请求头,只有17个真的只把那一段给了回来,90个当没看见、照样把整份文档推过来。声明了Accept-Ranges的一共7个,其中2个说了却给不出;反过来11个一声不吭却给得出。还有4个站同时干了两件互相打架的事:状态码写200,头里却带着Content-Range,实体只有全文的千分之几。同一批站上的CSS文件却有81/106规规矩矩回206——同一个请求头,文档和静态文件是两种命运。
上一轮量HTTP/2的帧,问的是“这份东西被切成了几块”,答案是客户端和服务器各出一半。这一轮换个问法:我能不能主动只要其中一段?
这件事在协议里有正经写法。请求头里加一行Range,服务器如果认,就回206加一个Content-Range,把那一段字节交给你;如果不认,就当没看见,回200和整份文档。视频拖进度条、下载断点续传、CDN把大文件切片缓存、爬虫抓大文件的头部,走的都是这一条。它跟请求头里那些被反复重发的字节不一样:那些是你不得不带上的开销,这一行是你主动加的一句问话。
它跟上一轮那件事其实是同一个问题的两头。上一轮是服务器替你切,切法你插不上话;这一轮是你主动要一段,能不能拿到全看对方认不认。两头合起来才是完整的答案:一份资源被怎么分次交付,从来不是它自己一家说了算。
问题是,现网到底有几个站认这一行。
一个标着200的响应,正文只有全文的千分之几?
先看最离谱的那两个站
kotn.com的首页,加一行Range: bytes=0-1023发过去,回来的是HTTP 200。按规范,200的意思是“这是完整的东西”。可同一个响应的头里还挂着Content-Range: bytes 0-1023/1524288,实体只有435字节的压缩流,解开来正好1024字节。一个标着200的响应,装的是全文的千分之零点七。
bigcommerce.com是同一种毛病,Content-Range: bytes 0-1023/1304781,状态码200,实体645字节。两个站分别在Vercel和Cloudflare后面,不是同一家实现的偶发问题。
规范其实把话说死了
RFC 9110里关于Content-Range的那一节写得很清楚:这个字段只出现在206响应里,或者出现在416那种“范围不满足”的响应里,用来告诉对方真实长度。200配Content-Range是个没有定义的组合,规范没说该怎么解释,客户端也就各解释各的。
这为什么要紧?因为绝大多数客户端只看状态码。看到200就认为拿到了完整文档,往下走解析、抽正文、算相似度。首页HTML里到底有多少可读正文那一轮已经讲过,一份被截断的HTML在下游很难被发现——它不报错,只是内容少了一大半,指标悄悄往下走。
这不是我的客户端搞错了
第一反应当然是怀疑自己。我把响应体按原始字节读了一遍,不解压、不跟随重定向:kotn那次回来的确实是435个字节的brotli流,而正常请求同一个地址回来的是99930字节。头里那个1524288则是解压之后的全文长度。三个数字各自都对,凑在一起就是一份自相矛盾的响应。
Accept-Ranges这句声明,有多少是真的?
先看有多少站愿意开口
服务器要是支持范围请求,规范建议它在普通响应里带一行Accept-Ranges: bytes,等于提前打个招呼;MDN对这一行的说明里还写了另一个取值none,意思是明说“别问了,我不支持”。107个可用的首页里,带了bytes这一行的只有7个。
但“不说”不等于“不给”。把声明和实做交叉起来看,格子里的分布比想象的乱:
| 普通响应里的声明 | 发Range之后实际给的 | 站数 | 怎么理解 |
|---|---|---|---|
| Accept-Ranges: bytes | 206,只给那一段 | 5 | 说到做到 |
| Accept-Ranges: bytes | 200,给了全量 | 2 | 说了却给不出 |
| 什么都没说 | 206,只给那一段 | 11 | 不吭声但给得出 |
| 什么都没说 | 200,给了全量 | 88 | 没说也没给,唯一自洽的一格 |
| Accept-Ranges: none | 206,只给那一段 | 1 | 明说不支持,实际支持 |
先看第三格。11个站从来没说过自己支持,问它要一段却给得干干净净——anker.com、buckmason.com、dji.com、eufy.com、ikea.com、insta360.com等都在这一格。这说明Accept-Ranges这一行的缺席,不能拿来判断服务器支不支持,想知道答案只有一个办法,发一次真请求。
第二格只有2个站,arcteryx.com、vaude.com。它在普通响应里挂着Accept-Ranges: bytes,真发Range过去却回了整份文档。声明和实做之间隔着一层边缘缓存,声明是源站写的,实做是边缘做的。
还有一格更别扭:1个站(rab.equipment)在普通响应里写的是Accept-Ranges: none,等于明说“我不支持”,可真发Range过去,它照样回了206、照样只给那一段。明着否认的那一行,同样不能当判据用。
为什么“沉默”这一格这么大
88个站既不声明也不支持,占了82.2%。这倒不算配置错误——首页是动态生成的,服务器边渲染边往外送,压根不知道最终有多长,自然也没法交出某一段。这跟上一轮量到的现象是一回事:相当一批站连Content-Length都报不出来,因为整份文档在开始发的时候还没生成完。
换句话说,文档层不支持范围请求,多半不是不想支持,是没法支持。真正值得追问的是另外那几格:声明和实做为什么会各走各的。
声明是源站写的,实做是边缘做的
这几格错位的成因,说穿了就是响应在路上被改过。源站把Accept-Ranges写进响应头,边缘节点为了压缩、为了流式转发、为了拼自己的东西,把响应体重新组织了一遍,那一行却原样透传了下来。Vary这个头也是同一种命运:写它的人和最终决定响应长什么样的人,不是同一个。robots写的和送达层做的对不上那一轮,量的也是这条缝。
同一个地址,只换一个Accept-Encoding,为什么答案不一样?
6个站在两遍之间翻了面
这一轮我对每个首页问了两遍同样的Range:一遍带Accept-Encoding: gzip, deflate, br,一遍写identity明说不要压缩。
翻面的方向还不止一种:5个站是带压缩时回全量、去掉压缩才肯给那一段(article.com、boohoo.com、braun.com、notino.com等);1个站反过来,带压缩时给了206、明说不要压缩反而回了全量(jysk.com)。同一个地址、同一台服务器、前后差两秒,答案取决于你在另一个请求头里写了什么。
在自己的nginx上,这件事能一个开关一个开关地复现
我在自己的机器上另起了一个独立的nginx实例,跑在几个不占用的端口上,服务同一个376890字节的CSS文件,配置之间只差压缩这一项:
| 服务器配置 | 普通响应 | 带压缩的Range | 不带压缩的Range |
|---|---|---|---|
| gzip off | Accept-Ranges: bytes | 206,分母376890 | 206,分母376890 |
| gzip on(边压边发) | 连Accept-Ranges都不发 | 200,全量 | 206,分母376890 |
| gzip_static on(预压好的文件) | Accept-Ranges: bytes | 206,分母15250 | 206,分母376890 |
中间那一行是关键:一旦开了动态压缩,nginx会主动把Accept-Ranges这一行撤掉,并且对带压缩的Range请求直接回全量。原因也很实在——它是边压边发的,压缩流的第几个字节对应原文第几个字节,它自己也算不准。压缩那一轮量的是这套配置能省多少字节,这一轮量的是它顺手拿走了什么。条件请求那一轮也撞见过它的另一个副作用:开压缩会把ETag换掉。
第三行则是另一条路:文件事先压好放在那儿,长度是确定的,于是范围请求又活了过来,只不过量的是压缩文件的字节。这也解释了现网那11个“不吭声却给得出”的站——它们交出去的多半是边缘缓存里那份已经定型的副本。
Content-Range后面那个分母,量的是压缩前还是压缩后?
这是本轮最容易被拿去当数据用、又最容易错的一个字段。Content-Range: bytes 0-1023/1524288里的1524288,规范里叫“完整表示的长度”。麻烦在于,“完整表示”指的是哪一份。MDN在206那一页把它描述成整个资源的大小,可实际拿到手的数字,取决于这一次内容协商谈出了什么。
本轮拿到Content-Range的一共20个,其中15个的分母明显大于这次实际过线的字节数——因为这次响应是压缩过的,而分母描述的是解压之后那一份。
| 站点 | Content-Range里的分母 | 这次实际过线的字节 | 倍数 |
|---|---|---|---|
| kotn.com | 1524288 | 99930 | 15.254倍 |
| bigcommerce.com | 1304781 | 95934 | 13.601倍 |
| segway.com | 232516 | 26424 | 8.799倍 |
| vuoriclothing.com | 533016 | 62952 | 8.467倍 |
| traeger.com | 789190 | 96656 | 8.165倍 |
| anker.com | 1920319 | 250421 | 7.668倍 |
| babybjorn.com | 716135 | 95825 | 7.473倍 |
| soundcore.com | 1904748 | 260556 | 7.31倍 |
实验台上这件事能一次看清:同一个big.css,预压缩那一档,带gzip问过去分母是15250,换identity再问同一个地址,分母变成376890。同一个URL、同一台服务器,“总长度”在两种请求下差了24.7倍。
所以这个分母不能直接拿去当文件大小用。它只在同一次协商的上下文里成立,换个请求头就换了一个坐标系。响应头自相矛盾那一轮收集的“矛盾”,有相当一部分是这么读出来的:两个字段各自都对,只是描述的不是同一份东西。图片格式协商那一轮的坑也一样,同一个地址返回的到底是哪种格式,取决于你在请求里写了什么。
要末尾一百字节、要两段、要一段不存在的,分别会怎样?
三种写法,三种冷淡
Range这个头不止“从第几字节到第几字节”一种写法。RFC 9110把单段、多段和越界三种情形都写进了语法里。我照着又问了三个问题,答案一个比一个说明问题。
| 问法 | 意思 | 206 | 200 | 416 |
|---|---|---|---|---|
| bytes=0-1023 | 要开头1KB | 17 | 90 | 0 |
| bytes=-100 | 只要末尾100字节 | 21 | 86 | 0 |
| bytes=0-99,500-599 | 一次要两段 | 11 | 93 | 3 |
| bytes=999999999- | 故意越界 | 1 | 84 | 22 |
“只要末尾100字节”这个写法,21个站认,回来的正好是100字节。它跟前面那问的支持面基本重合,说明服务器要么整套认,要么整套不认,没有只支持一半的。而“一次要两段”只有11个站给了206,其中真的按multipart格式回的只有9个。
越界那一问最值得看
我故意要了一段根本不存在的字节。规范说这时候该回416,并且带上真实长度。22个站这么做了,84个站直接把整份文档推了过来,中位数727727字节。回全量本身不算错得离谱,但它说明这些服务器压根没解析Range这一行——不是判断之后拒绝,是从头到尾没看见。这跟前面那88个“没说也没给”的站是同一批。
这里能顺出一条挺好用的判据:想知道一台服务器有没有真的在读Range这一行,不要问它一个合法的范围,问它一个不可能满足的范围。认真解析的会回416并告诉你真实长度,没解析的会把整份文档推过来,两者一眼可辨。合法范围反而分不出来——回200既可能是“我看懂了但给不了”,也可能是“我根本没看”。
静态资源那一侧,为什么反而正常?
同一个请求头,两种命运
把同一批站上的CSS和JS挑出来,用一模一样的请求头再问一遍,画风完全变了。
| 目标 | 样本 | 声明了Accept-Ranges | Range回206 | 一次要两段也给 |
|---|---|---|---|---|
| 首页文档 | 107 | 7 | 17 | 9 |
| CSS文件 | 106 | 72 | 81 | 62 |
| JS文件 | 95 | 54 | 64 | 46 |
CSS这一行的206比例是81/106,JS是64/95,而首页文档只有17/107。同一批站、同一个请求头,文档层和资源层的支持率差了好几倍。连“一次要两段”这种冷门写法,CSS都有62个站规规矩矩地回了multipart。
差别的根子还是那一条:静态文件躺在磁盘上或者边缘缓存里,长度确定、内容不变,服务器随时能从中间切一刀;首页是现做的,它自己都不知道有多长。静态资源那一轮关心的是这些地址能活多久,这一轮关心的是它们能不能被切开——两件事的前提是同一个:它们是文件,不是流。
CSS比JS高出来的那几个百分点
两类文件的206比例不一样,这不是巧合。JS更容易挂在带动态压缩的应用服务器后面,也更容易带着查询参数走一层网关;CSS则多半是纯静态产物,直接由边缘节点出。CSS那一轮顺带量过这些文件有多大,动辄几十上百KB——它们恰恰是最有可能被切片缓存拿去分块的那一类。
顺手撞到的一个口径问题
这些资源地址是从两天前的首页语料里抽出来的,25个已经失效——带哈希的文件名换了一版,或者被参数校验挡了回来。这类地址一旦失效就是404或者400,我在统计里把它们剔掉了,只算普通请求能拿到200的那些。做基线对照的时候这一步不能省,否则失效地址会被算成“不支持范围请求”,把支持率压低一大截。
按平台看,这件事几乎是被几家决定的
把可用的首页按响应头里的服务器分组,这一维的分布跟上一轮量帧的时候一样整齐。
| 响应头里的服务器 | 可用站数 | 首页Range回206的 |
|---|---|---|
| cloudflare | 78 | 3 |
| 未标识 | 9 | 4 |
| netlify | 6 | 5 |
| cloudfront | 3 | 0 |
| vercel | 3 | 1 |
| tengine | 2 | 2 |
| openresty | 2 | 1 |
| envoy | 1 | 0 |
这跟上一轮的结论是同一件事的两面:帧大小那一轮量到的所谓“现网分布”,其实是几家托管平台默认配置的分布。范围请求这一维也一样,换一家托管,这一行的行为就换一套,跟你自己写的代码基本无关。服务器配置那份清单里能自己拍板的项,到了托管平台上已经少了一大半。
爬虫会不会真的发Range?这件事什么时候伤到收录?
先说不会疼的部分
抓一个普通HTML页面,爬虫不会发Range,它就是一个完整的GET。所以上面那90个“不支持”的站,在日常抓取里一点问题都没有。真正能省下整份文档的是条件请求,不是范围请求;那一轮量到的142个站里只有52个能正确回304,那才是抓取预算上真金白银的浪费。
Google对文档层的态度也很直白:Googlebot只处理每次抓取的前15MB,超出的部分直接丢掉,而不是回头再发一个Range去要剩下的。抓取上限那一轮算过这条线对绝大多数页面意味着什么,首页字节预算那一轮则量过真被截断的是哪几个站。
再说真会疼的三种场景
第一种是视频。播放器要拖进度条、要边下边播,靠的就是范围请求;不支持的地址在很多播放器里干脆不能seek。首页视频那一轮量过这些文件有多大,单个文件的中位数就有四百多万字节,这种体量的东西不给切,用户体验直接塌掉。
第二种是被截断却标成200的响应,也就是开头那两个站。任何按状态码判断完整性的下游——自己的采集器、监控脚本、内容比对流程——都会被它骗过去,而且骗得悄无声息。
第三种是大文件下载中断之后接不上。用户下一份几十兆的手册、固件或者安装包,网一断就得从头来。移动网络下这种情况一点都不少见,而它在办公室的宽带上永远复现不出来。
还有一层容易被忽略的:CDN自己在用
不少CDN把大文件切成固定大小的块分别缓存,靠的就是往源站发范围请求,nginx的slice模块就是这么干的。源站要是不支持,切片缓存会直接退化成每次回源拉全量——缓存命中率的账面数字可能还挺好看,回源带宽却下不来。多层缓存那一轮讲过这类“看着命中了其实没省”的形态。
我这套量法在哪几处会骗人?
四个已经踩实的坑
- 只看状态码不看响应体。差点写成“kotn支持范围请求,回了200”。把原始字节数出来才看清,435对99930。
- 只用一种Accept-Encoding问。6个站的结论会因此反过来,同一个地址得问两遍,一遍写identity。
- 把Content-Range的分母当文件大小。压缩前和压缩后会被混进同一列,实验台上同一个文件能量出两个分母。
- 拿失效的资源地址算支持率。25个404会被算成“不支持”,得先确认普通请求是200,再问Range。
三处说在前头的边界
一是这批站有78个在同一家边缘网络后面,分布更像是几家平台的默认配置,不是全互联网的样子。自建服务器的样本只有个位数。
二是每个站只问了一次。范围请求的状态码属于“取自一份东西”的读数,不像时间那样每次都变,但边缘节点当时缓没缓住这份文档,确实可能改变答案——这一点我没有跑第二轮去证伪,写在这里当作已知的口径缺口。上一轮的教训是取自某个时刻的读数必须跑两遍;这一轮的读数不取自时刻,但取自缓存状态,严格说也该复核一遍。
三是资源层每个站只抽了一个CSS和一个JS,抽的是首页里第一个出现的那个。它代表不了全站,只能说明这个站的静态文件走的是哪一套。
自己站上怎么查、怎么配
三条命令看清现状
判断自己站在哪一格,不需要写脚本:
curl -s -o /dev/null -D - https://你的域名/ | grep -i accept-ranges
curl -s -o /dev/null -D - -H 'Range: bytes=0-1023' https://你的域名/ | head -20
curl -s -o /dev/null -D - -H 'Range: bytes=999999999-' https://你的域名/某个.css | head -20第三条就是上面那个判据:拿一个不可能满足的范围去问,回416说明服务器真的在解析,回200说明它压根没看。另外别用-I只发HEAD,很多站给HEAD的头跟GET不是一套,这个坑单独踩过一次。
该配什么,不该配什么
动态HTML页面不用管。没人会对一个首页发Range,不支持是常态,为它去关压缩得不偿失。
静态资源尽量让它保持可切。nginx上最省事的做法是用gzip_static而不是动态gzip:预先压好的文件长度确定,Accept-Ranges不会被撤掉,压缩和范围请求可以同时存在。代价是构建流程里要多一步生成.gz文件。
视频和大文件一定要支持。这类东西通常直接从对象存储或者CDN出,默认就支持;真正要小心的是自己用后端程序代理输出的那种,很多框架的文件下载接口是一次性把整个文件读进内存再吐出去,既不发Accept-Ranges,也不认Range。
用了CDN切片缓存,回源那一段必须支持。否则切片模块会一直拉全量,白白多花回源带宽。这一项在自己的监控里很难看出来,得去翻回源日志,看请求有没有带Range。
一个真实的排查
保哥去年帮一个做智能硬件的独立站查过一次固件下载的投诉,用户反馈“下到一半断了就得从头来”。查下来是下载接口用后端程序读文件再输出,响应头里既没有Accept-Ranges也不认Range,换成直接由nginx出静态文件之后问题就没了。这类问题的排查成本远高于修复成本,因为它在办公室的网络里永远复现不出来,只有断网重连的用户才碰得到,而他们通常不会来报障,只会不再下载。
常见问题解答
首页不支持范围请求,会影响SEO吗?
基本不会。爬虫抓HTML发的是完整GET,不会发Range。本轮107个首页里90个不支持,这在动态页面上是常态。真正跟抓取预算有关的是条件请求和压缩,不是范围请求。
为什么我开了gzip之后Accept-Ranges就没了?
这是nginx的主动行为,不是bug。动态压缩是边压边发的,压缩流的第几个字节对应原文第几个字节它自己也算不准,所以干脆撤掉声明并对带压缩的Range请求回全量。想两者兼得就用预压缩,把.gz文件事先生成好,配gzip_static。
状态码200却带着Content-Range,客户端会怎么处理?
绝大多数客户端只认状态码,会把那一小段当成完整文档收下。这类响应最危险的地方在于它不报错,下游拿到的是一份被悄悄截断的内容。如果你在做内容比对或者采集,判据应该加一条:拿到200的时候顺手看看有没有Content-Range,有就说明这不是全量。
Content-Range里的总长度能当文件大小用吗?
不能直接用。它描述的是这一次协商出来的那份表示有多长,压缩响应给的是压缩后的长度,不压缩给的是原文长度。本轮实验台上同一个文件量出15250和376890两个分母,差24.7倍。要文件大小,请用不带压缩的普通请求去看Content-Length。
越界的Range应该返回什么?
规范说该回416,并且带上Content-Range: bytes */总长度告诉对方真实长度。本轮22个站这么做了,84个站直接把全量推了过来。回全量不算致命,但它意味着这台服务器压根没解析这个请求头。
一次要两段的多段范围,值得支持吗?
对普通网站没什么必要。文档层真给了multipart/byteranges的本轮只有9个站。这个特性主要用在PDF按页取、视频按索引取这类场景,普通页面和资源用不上,不支持也不算缺陷。
视频文件一定要支持范围请求吗?
要。播放器拖进度条靠的就是它,不支持的话很多播放器直接禁用seek,或者每次拖动都从头重下。视频通常从对象存储或CDN出,默认支持;自己用后端程序代理输出的要专门检查一遍。
CDN会不会替我把这件事解决了?
对静态资源多半会,对动态页面不会。本轮数据里同一家边缘网络后面的站,静态资源的206比例明显高于首页文档,说明边缘缓存住的那份是有确定长度的文件,能切;没缓存住的动态响应还是回源现取,切不了。
这批数字能代表我的站吗?
机制能,比例不能。“动态页面切不了、静态文件切得了”“开了动态压缩就没有范围请求”“分母跟着协商结果走”这几条是机制,换个样本也成立。至于17比90这个比例,只描述这107个站在这一刻的样子。
权威参考资料
本文标题:《Range请求回了200,里面却连全文的千分之一都不到》
本文链接:https://zhangwenbao.com/range-request-partial-content-audit.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0
← 上一篇
DOM元素过多实测:112个首页68.8%越过1400这条线下一篇 →
没有了