登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

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。

Range 请求字段与部分响应材料的静态关系
图1:范围响应结构图。请求条件和响应材料必须匹配,图示不是实际抓包记录。

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;换成可变文件后,必须确保标签随表示内容变化,不能继续沿用示例常量。

下载处理器与可定位资源的静态依赖结构
图2:处理器结构图。ServeContent 使用请求条件和独立 Reader 组织响应,不代表代码已运行。

下载交互应区分继续、重下和失败

先在一个终端运行服务,再在另一个终端执行请求。以下命令只供读者手动操作,不是文章制作过程中已经运行的检查。

# 在保存 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 包装当成文件字节。

参考:Go ServeContent 文档;HTTP Range 语义;If-Range 条件。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>