编程 Go 1.28 的 Range 请求超过 200 段就整段忽略:断点续传与分片下载会退回整包

2026-11-06 21:02:02

Go 1.28 的 Range 请求超过 200 段就整段忽略:断点续传与分片下载会退回整包

Go 1.28 改了 net/http 里处理 Range 请求的一组入口:FileServer、FileServerFS、ServeContent、ServeFile、ServeFileFS。当请求头里的 Range 段数超过 httpservecontentmaxranges 时,整个 Range 头被忽略,服务器按普通请求返回 200 和完整 Content-Length,而不是 206 Partial Content。默认值是 httpservecontentmaxranges=200,设为 0 关闭限制。该设置和默认值已回溯到 Go 1.27.2 与 Go 1.26.9。

限制的是什么

限制对象是 Range 头里的段数,不是单段字节范围的大小。一个请求写 bytes=0-99 是一段;写 bytes=0-99,200-299,400-499 是三段。超过 200 段(例如 201 段)时,net/http 不返回 416、不返回 400,也不截断到前 200 段,而是直接忽略整个 Range 头。

设 httpservecontentmaxranges=0 恢复旧行为,即不限制段数。

为什么这是 DoS 缓解

多段 Range 的成本在服务端,不在请求体。一个很小的请求头可以带几千段范围,服务端要逐段解析、校验、排序合并,再为每段定位文件偏移、写出边界和头部。响应体变成 multipart/byteranges,内存占用和 CPU 时间随段数增长,而客户端发的字节数几乎不变。这是典型的放大面:攻击者用低成本请求换服务端高成本响应。

上限把单次请求的处理成本截断在 200 段,超过就退回整包。整包响应虽然字节多,但服务端路径简单,不会因为段数膨胀而消耗额外解析和拼接资源。

表现:客户端看到 200,而不是报错

忽略 Range 头的结果是“看起来正常”的 200:

  • 响应状态行是 HTTP/1.1 200 OK;
  • Content-Length 是文件全量大小,不是分片长度;
  • 没有 Content-Range,也没有 multipart/byteranges。

对下载器来说,这比报错更隐蔽。断点续传逻辑通常发 Range: bytes=- 期望 206,拿到 200 后有的实现会从头覆盖写,有的会认为服务端不支持 Range 而放弃续传,最终同一个文件被整包重复下载。分片下载器如果一次拼接大量小段,超过上限后每一段请求都退化成整包,流量和耗时都会上去。

怎么判断自己中招

几个可观察信号:

  • 访问日志里同一个文件在短时间内被多次整包下载,响应字节数等于文件大小,而不是请求的范围长度;
  • CDN 回源流量上升,源站看到的分片请求变成全量请求;
  • 日志或监控里 206 的比例下降,200 的比例上升,尤其是大文件路径;
  • 下载类客户端反馈“续传不生效”“每次都从头下”。

这些信号同时出现时,先确认客户端是否在单个 Range 头里塞了超过 200 段。

验证命令

先起一个静态文件服务:

package main

import (
"log"
"net/http"
)

func main() {
// Go 1.28 默认 httpservecontentmaxranges=200
// 超过 200 段的 Range 头会被整段忽略,返回 200 全量
log.Fatal(http.ListenAndServe(":8080", http.FileServer(http.Dir("./static"))))
}

单段请求应当是 206:

curl -s -o /dev/null -D - -r 0-99 http://127.0.0.1:8080/big.bin

构造 201 段请求,观察是否退回 200:

RANGES=$(python3 - <<'EOF'
print(",".join(f"{i*100}-{i*100+9}" for i in range(201)))
EOF
)
curl -s -o /dev/null -D - -H "Range: bytes=$RANGES" http://127.0.0.1:8080/big.bin

只看状态码:

curl -s -o /dev/null -w '%{http_code}\n' -H "Range: bytes=$RANGES" http://127.0.0.1:8080/big.bin

抓包确认响应行是 200 还是 206:

sudo tcpdump -i any -A -s 0 'tcp port 8080' | grep -E 'HTTP/1\.[01] (200|206)'

把段数降到 200 以内再跑一次,状态码应回到 206,这样可以排除文件不存在、路径错误等干扰。

设置方式与取舍

httpservecontentmaxranges 是 GODEBUG 设置,三种固定方式:

环境变量:

GODEBUG=httpservecontentmaxranges=0 ./server

go.mod 的 godebug 块:

godebug (
httpservecontentmaxranges=0
)

或源码里的 //go:debug 指令:

//go:debug httpservecontentmaxranges=0
package main

设为 0 关闭限制,适合内网、客户端可信、或确实需要单请求大量分片的场景。代价是重新暴露多段 Range 的放大面:任何能发请求的客户端都可以用几千段的范围头推高服务端 CPU 和内存。公网文件服务保留默认 200 更稳妥,需要大量分片的客户端改成分多次请求,每次控制在 200 段以内。

回溯意味着升级到 Go 1.27.2 或 1.26.9 也会带上这个限制,不需要改代码,但行为会变。观察是否走到非默认行为,可以读 runtime/metrics 里的 /godebug/non-default-behavior/httpservecontentmaxranges:events 计数器;显式设置过该值时,这个计数器会记录事件。

复制全文 生成海报 Go HTTP 文件下载 GODEBUG

推荐文章

程序员茄子在线接单