flask需手动解析range头、校验范围合法性、定位文件偏移,用生成器流式yield二进制数据,设置206状态码及content-range等响应头,禁用send_file,避免内存溢出与缓存干扰。

Flask 怎么正确响应 Range 请求实现断点续传
Flask 默认不处理 Range 请求头,直接返回 200 和完整文件,客户端无法续传。必须手动解析 Range、计算偏移量、设置 Content-Range 和状态码为 206。
关键不是“能不能”,而是“怎么避免踩坑”:比如忽略 If-Range 头、用 send_file 硬套、没校验范围合法性,都会导致客户端静默失败或卡死。
- 先读取文件大小(
os.path.getsize),不能依赖stat后再打开,避免竞态 - 从
request.headers.get("Range")提取字节范围,格式如"bytes=1000-1999",需正则提取并校验上下界 - 若范围无效(如
start > end或end >= filesize),返回 416Range Not Satisfiable - 响应必须包含
Content-Range(如"bytes 1000-1999/10000")、Content-Length(本次响应体长度)、Accept-Ranges: bytes
为什么不能直接用 send_file 做断点续传
send_file 内部不检查 Range,也不会设 206 状态码,即使你手动改了状态码,它仍会把整个文件发出去——客户端收到 206 但 body 是全量内容,校验失败后直接放弃续传。
更隐蔽的问题是:它默认使用 as_attachment=True 时会加 Content-Disposition,但断点续传场景下这个头不是必需的,反而可能干扰某些下载器解析 Content-Range。
- 要用
Response手动构造流式响应,配合open(..., "rb")+seek()+read() - 务必设
direct_passthrough=True,否则 Flask 可能缓冲整个 chunk,失去流式意义 - 别忘了设
content_type,尤其对无扩展名文件,否则浏览器可能无法识别 MIME 类型
If-Range 头怎么判断要不要重置续传
客户端发 If-Range 是为了确认服务端文件没变过——如果值是 ETag,就比对;如果是时间戳,就比 Last-Modified。一旦不匹配,服务端必须忽略 Range,退化为 200 全量响应。
很多人漏掉这步,结果客户端以为文件没变,继续从旧偏移下载,拿到的却是新文件的错位数据。
- 生成 ETag 推荐用
base64.b64encode(hashlib.md5(file_path.encode()).digest()).decode(),比时间戳更可靠 - 如果请求带
If-Range且和当前 ETag 不一致,直接 returnsend_file(..., as_attachment=True)(此时走 200) - 注意:ETag 必须加引号,如
f'"{etag}"',否则部分客户端不认
大文件传输时的性能和内存陷阱
用 seek() + read() 看似简单,但若 chunk 太大(比如 100MB),Python 进程会瞬间吃光内存;若 chunk 太小(比如 1KB),IO 次数爆炸,吞吐暴跌。
实际测试中,8KB–64KB 是较稳的区间,具体看磁盘类型和并发量。另外,别在响应里用 json.dumps 或其他序列化操作——它们会强制加载全部内容到内存。
- 用
yield分块生成响应体,配合Response(..., direct_passthrough=True) - 每次
read(chunk_size)后立即yield,不要拼接字符串或 bytes 对象 - Linux 下可考虑
os.sendfile()(需 Python 3.7+ 且非 WSGI 环境),但 Gunicorn/uWSGI 不支持,慎用
最易被忽略的是:Nginx 或 CDN 层可能缓存了 200 响应,却透传了 206 ——结果用户第一次下载走 CDN,续传时被回源,ETag 对不上。这种问题只能靠 Cache-Control: no-store 强制绕过中间缓存。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











