Go HTTP Range 请求为什么返回 200 而不是 206
来源:17golang原创
时间:2026-10-23 02:56:59 500浏览 收藏
Go HTTP 请求带了 Range 却收到 200,最常见的原因是处理器只写出了完整内容,并没有实现范围响应。设置 Accept-Ranges: bytes 也不会自动把完整下载变成部分下载。另一个正常情况是 If-Range 与当前资源不匹配,服务端应回退为完整响应,避免客户端拼接两个版本。
官方地址:https://pkg.go.dev/net/http
先确认客户端实际发送的是 GET 和有效的 bytes 范围,再确认处理器是否使用 ServeContent 等范围感知实现。不要只把状态码改成 206;响应体和 Content-Range 必须对应同一段内容。
断点续传需要的不只是一个请求头
用户点击“继续下载”时,客户端通常已经保存一段内容,希望只取剩余字节。服务器收到 Range 是收到一个范围请求,不是必须返回 206 的承诺。HTTP 允许忽略范围请求,因此客户端要能够处理 200。对不支持 Range 的自定义接口,完整返回可能符合协议,但无法提供预期的续传体验。
对下载组件而言,最危险的不是显示“重新下载”,而是把 200 的完整响应直接追加到旧文件末尾。这样界面可能显示进度增加,最终文件却已损坏。所以排错要同时看状态码、范围字段、资源版本和实际字节,不能把“HTTP 请求成功”当成“可以续传”。
先把请求与响应字段对应起来
本例使用固定的 16 字节 ASCII 资源。请求 Range: bytes=0-3 表示从零开始、包含结束位置的四个字节。在单段范围被正常接受时,应得到 206、Content-Range: bytes 0-3/16,正文为 0123。Content-Length 若存在,表示本次响应内容长度,不是完整资源长度。
Range 定义在 GET 上;不要使用 curl -I 发送 HEAD 来代替真实范围下载观察。还要确认最终地址:登录跳转、对象下载重定向、反向代理可能使实际处理请求的服务不是你正在修改的 Go handler。

If-Range 表示“版本没变就给这一段,变了就给完整内容”。使用实体标签时应使用强 ETag,不能把弱标签当成可拼接文件的保证。资源更新后返回 200 并不一定是故障;客户端需要丢弃旧的续传计划并重新获取当前完整版本。
用 ServeContent 实现范围感知的下载接口
下面是本次原创的完整演示程序,保存为 main.go 即可供读者运行。它同时提供只写完整正文的反例 /plain 和支持范围的 /ranged。本文没有启动服务或执行这些请求;下文结果均为对固定示例的预期说明。
package main
import (
"errors"
"io"
"log"
"net/http"
"strings"
"time"
)
// 固定资源及其强 ETag 必须始终一一对应;更改资源也要更改 ETag。
const payload = "0123456789abcdef"
const resourceTag = `"range-demo-v1"`
func allowRead(w http.ResponseWriter, r *http.Request) bool {
if r.Method == http.MethodGet || r.Method == http.MethodHead {
return true
}
w.Header().Set("Allow", "GET, HEAD")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return false
}
func plain(w http.ResponseWriter, r *http.Request) {
if !allowRead(w, r) { return }
// 反例:声明支持 bytes 不等于实现了范围处理。
w.Header().Set("Accept-Ranges", "bytes")
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
if r.Method == http.MethodHead { return }
if _, err := io.WriteString(w, payload); err != nil {
log.Printf("write response: %v", err)
}
}
func ranged(w http.ResponseWriter, r *http.Request) {
if !allowRead(w, r) { return }
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("ETag", resourceTag)
// 每次请求独立游标;交给 ServeContent 判断条件、范围和状态码。
// 零修改时间不生成 Last-Modified,此例只使用强 ETag。
http.ServeContent(w, r, "sample.txt", time.Time{}, strings.NewReader(payload))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/plain", plain)
mux.HandleFunc("/ranged", ranged)
// 只监听本机,超时适用于这个极小演示,不代表大文件生产配置。
server := &http.Server{
Addr: "127.0.0.1:8087", Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
}
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}
ServeContent 接收可读且可定位的 ReadSeeker。strings.NewReader 适合这个固定小资源,每个请求创建独立游标;真实大文件可使用按请求打开的文件,并由调用方关闭文件。不要把普通远端流强行当作可定位内容,也不要把大文件全部读入内存只为适配示例。
调用 ServeContent 前不要先 WriteHeader(200),也不要先写提示文字:响应一旦提交,后续不能再正常选择 206 或其他状态。ETag 也应在调用前设置。本例 ETag 只适用于完全固定的 payload;换成可变文件后,必须确保标签随表示内容变化,不能继续沿用示例常量。

下载交互应区分继续、重下和失败
先在一个终端运行服务,再在另一个终端执行请求。以下命令只供读者手动操作,不是文章制作过程中已经运行的检查。
# 在保存 main.go 的目录启动本机示例 go run main.go
# 查看反例:虽然声明 Accept-Ranges,仍完整返回 curl -i -H 'Range: bytes=0-3' http://127.0.0.1:8087/plain # 正常单段请求:预期 206,正文四个字节 curl -i -H 'Range: bytes=0-3' http://127.0.0.1:8087/ranged # 强 ETag 匹配:预期保留范围请求 curl -i -H 'Range: bytes=0-3' -H 'If-Range: "range-demo-v1"' http://127.0.0.1:8087/ranged # 旧版本标签不匹配:预期 200 和完整十六字节正文 curl -i -H 'Range: bytes=0-3' -H 'If-Range: "old-demo"' http://127.0.0.1:8087/ranged # 超出非空资源的单段范围:预期 416 curl -i -H 'Range: bytes=100-' http://127.0.0.1:8087/ranged
| 响应 | 下载组件应做什么 |
|---|---|
| 206,单段 Content-Range 符合请求 | 确认版本和偏移后接收该段,不盲目按当前文件末尾追加 |
| 200 | 作为完整响应处理,不追加到旧版本残片 |
| 416 | 重新判断远端长度和本地偏移,不等同于文件已完整 |
| 304 或 412 | 按缓存或前置条件语义处理,不当作下载正文 |
界面上应分别给出“继续下载”“资源已更新,重新下载”和可重试错误,不要统一显示“续传成功”。进度提示使用文字同时说明状态,不能只靠颜色;键盘可操作的重试或重新下载控件,也比遇到 200 就循环重试更容易理解。
直连正常但经过代理仍返回 200 时看哪里
在自己的受控环境中,分别记录直连源站与正式链路收到的状态和关键头,比较 Range、If-Range、Content-Encoding、Content-Range 是否在同一表示上保持一致。只看到源站日志有 Range,还不足以说明客户端拿到的内容就是源站那一段。
动态 gzip 等内容变换需要特别谨慎:字节偏移对应选中的表示,不应先对原始内容切片,再随意压缩并沿用原范围头。示例故意不加压缩中间件。实际部署应让压缩、缓存和范围处理采用明确一致的方案,而不是手工改几个头“凑成 206”。同时评估大文件超时与并发开销;示例的十秒写超时只用于极小资源,不是生产下载配置建议。
只设置 Accept-Ranges 能解决吗?
不能。它用于声明范围能力,实际读取哪段、返回什么状态和字段仍由实现负责。
为什么没有 If-Range 仍然是 200?
优先确认 handler 没有自己完整写出正文、没有提前提交 200,以及请求是否真的到达支持范围的资源入口。然后再看代理与表示变换。
多个范围可以按普通文件片段直接追加吗?
不能。多段响应可能采用 multipart/byteranges,各段有自己的边界与范围信息;本文只演示单段,客户端不能把 multipart 包装当成文件字节。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
343 收藏
-
333 收藏
-
423 收藏
-
179 收藏
-
470 收藏
-
246 收藏
-
451 收藏
-
375 收藏
-
427 收藏
-
129 收藏
-
196 收藏
-
325 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习