直接用flask或fastapi默认响应无法支持断点续传,因为它们不解析range请求头、不返回206状态码及content-range等必要响应头,导致浏览器播放器无法分片加载,进而无法拖动进度条或恢复中断下载。

为什么直接用 Flask 或 FastAPI 的默认响应无法支持断点续传
浏览器发起视频播放请求时,会主动带上 Range 请求头(例如 Range: bytes=1024-2047),期望服务端只返回指定字节段。如果后端不解析该头、不返回 206 Partial Content 和对应 Content-Range 响应头,播放器就会认为资源不支持分片加载,进而无法拖动进度条或恢复中断的下载。
常见错误现象包括:Failed to load resource: net::ERR_CONTENT_LENGTH_MISMATCH、拖动后黑屏、播放器反复重试首帧。
- 必须手动读取
Range头并解析起始/结束偏移量 - 必须设置
Status: 206、Content-Range、Accept-Ranges: bytes、Content-Length(非整个文件,而是当前段长度) - 不能用
return send_file(...)这类封装函数——它们默认忽略Range,且返回200
如何用 FastAPI 手动实现带 Range 支持的流式响应
核心是绕过框架自动处理,用 StreamingResponse + 手动控制文件读取和响应头。以下逻辑适用于本地文件(如 /videos/sample.mp4),不依赖外部存储服务。
from fastapi import FastAPI, Request, HTTPException
from starlette.responses import StreamingResponse
import os
<p>app = FastAPI()</p><p>@app.get("/video/{filename}")
async def stream_video(request: Request, filename: str):
file_path = f"/videos/{filename}"
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="File not found")</p><pre class="brush:python;toolbar:false;">file_size = os.stat(file_path).st_size
range_header = request.headers.get("Range")
if not range_header:
# 不带 Range:全量返回(兼容普通下载)
def iter_file():
with open(file_path, "rb") as f:
yield from f
return StreamingResponse(iter_file(), media_type="video/mp4")
# 解析 Range: bytes=xxx-yyy
try:
range_value = range_header.strip().replace("bytes=", "")
start, end = range_value.split("-")
start = int(start) if start else 0
end = int(end) if end else file_size - 1
length = end - start + 1
except (ValueError, IndexError):
raise HTTPException(status_code=416, detail="Invalid range header")
if start >= file_size or end >= file_size or start > end:
raise HTTPException(status_code=416, detail="Range not satisfiable")
def iter_chunk():
with open(file_path, "rb") as f:
f.seek(start)
remaining = length
while remaining > 0:
chunk_size = min(8192, remaining)
data = f.read(chunk_size)
if not data:
break
yield data
remaining -= len(data)
headers = {
"Content-Range": f"bytes {start}-{end}/{file_size}",
"Accept-Ranges": "bytes",
"Content-Length": str(length),
"Content-Type": "video/mp4",
}
return StreamingResponse(iter_chunk(), status_code=206, headers=headers)
注意:iter_chunk() 必须是生成器函数,且每次 yield 的数据块不宜过大(8192 是经验值);太大易阻塞,太小则 syscall 开销高。
使用 nginx 做反向代理时为何断点续传失效
即使 Python 后端正确实现了 Range 响应,若前端经过 nginx,而配置中未显式开启字节范围支持,nginx 会吞掉 Range 头并转为完整请求,导致后端收不到 Range,最终返回 200 全量内容——浏览器收到后因 Content-Length 与 Content-Range 不匹配而报错。
- 必须在
location块中添加add_header Accept-Ranges bytes; - 确保没有配置
proxy_buffering off;以外的缓冲干扰(如proxy_buffer_size过小可能截断响应头) - 禁用
gzip或gunzip模块——压缩会破坏字节偏移,使Range失效
典型安全配置片段:
location /video/ {
proxy_pass http://backend;
add_header Accept-Ranges bytes;
proxy_http_version 1.1;
proxy_set_header Connection '';
gzip off;
}
真实部署中容易被忽略的三个细节
断点续传在开发环境跑通不等于生产可用。以下三点在压测或跨设备访问时高频暴露:
-
file_size必须用os.stat().st_size,不能缓存或硬编码——视频文件可能被后台任务覆盖更新,st_size变化但内存值未刷新,会导致Content-Range错误 - 并发请求下,多个
StreamingResponse同时open()同一文件不会冲突,但若用mmap或自定义缓冲区,需确保线程安全 - 移动端 Safari 对
Content-Range格式极其敏感:结尾必须有斜杠(bytes 0-1023/1024),不能少、不能多空格,否则静音播放或卡在 loading
最稳妥的做法是始终用 os.stat 动态获取大小,并严格按 RFC 7233 格式拼接 Content-Range 字符串——哪怕只是少了一个空格,iOS 就会放弃续传。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











