gin 本身不内置断点续传支持,需手动解析 range 请求头、调用 io.seeker 定位、设置 206 状态码及 content-range 等响应头实现;c.file() 底层用 http.servefile,只返回 200 响应且忽略 range,导致断点续传失效。

Gin 本身不内置断点续传支持,但可以通过手动解析 Range 请求头 + io.Seeker + 正确设置响应状态码与头部,实现标准 HTTP/1.1 断点续传(206 Partial Content)。
为什么 c.File() 无法支持断点续传
c.File() 是 Gin 封装的便捷方法,它内部调用 http.ServeFile,而后者只响应完整文件(200 OK),不会检查或处理 Range 请求头,也不返回 206 状态码。一旦客户端发起带 Range: bytes=1000-2000 的请求,c.File() 会直接忽略并返回整个文件(或 404/500),破坏续传逻辑。
- 现象:Chrome 下载大文件中途暂停后继续,进度条重置、网络面板显示 200 而非 206
- 根本原因:Gin 未暴露底层
http.ResponseWriter的写入控制权,也未做 Range 解析 - 替代方案必须绕过
c.File(),自己读取文件、计算偏移、设置响应头
手动实现 206 响应的核心步骤
关键在于把文件当作可随机访问的流(*os.File 实现 io.Seeker),按客户端请求的字节范围读取并写入 ResponseWriter。
- 用
c.Request.Header.Get("Range")提取原始 Range 头,格式如bytes=0-1023或bytes=1000- - 调用
filepath.Abs()获取绝对路径,并与白名单根目录比对,防止路径遍历(如../etc/passwd) - 用
os.Open()打开文件,得到*os.File—— 它支持Seek()和Read(),是实现分段读取的基础 - 解析 Range 后计算
start、end、length,注意end不能超过文件大小(stat.Size()) - 调用
c.Data()前,必须手动设置:c.Status(206)、c.Header("Content-Range", "bytes "+start+"-"+end+"/"+total)、c.Header("Accept-Ranges", "bytes")、c.Header("Content-Length", strconv.Itoa(length))
使用 c.DataFromReader() 的注意事项
c.DataFromReader() 是 Gin 提供的流式响应接口,适合封装自定义 Reader,但它**不自动处理 Range**,仍需你提前计算好起始偏移和长度,并构造一个只读指定区间的 io.Reader。
- 不要直接传入整个文件的
*os.File,否则会从头读取 —— 必须先file.Seek(start, 0) - 推荐用
io.LimitReader(file, int64(length))包裹,确保只读取目标字节段 - 务必在调用
c.DataFromReader()前设置所有必要 Header,否则浏览器无法识别为分块响应 - 若 Range 格式非法(如
bytes=100-50),应返回416 Range Not Satisfiable并设置Content-Range: */totalSize
容易被忽略的边界与兼容性问题
断点续传看似简单,但生产环境里最常栽在细节上:Range 头可能缺失、格式异常、超出文件长度,或者客户端根本不发 Range(比如某些旧版 wget)。这些情况都得兜底。
- 没有
Range头时,应降级为完整文件响应(200),而非报错 —— 这是 HTTP/1.1 兼容要求 - 文件大小为 0 时,
Content-Range必须写成bytes */0,不能是bytes 0-0/0 - Windows 下中文文件名用
filename="中文.zip"即可,filename*=在部分旧浏览器(IE)反而失效 - NGINX 作为反向代理时,默认会缓存 200 响应但不缓存 206;若需 CDN 支持续传,要显式配置
proxy_cache_key $scheme$host$request_uri并开启proxy_cache_valid 206 1h











