FastAPI实现分块上传需手动解析请求体而非使用UploadFile,通过Content-Range头定位偏移,校验X-Chunk-Hash确保完整性,按upload_id隔离存储并缓存已传范围以支持断点续传。

FastAPI 中如何接收分块上传的文件数据
断点续传本质是把大文件切片,客户端按顺序或并行上传多个 chunk,服务端需能独立接收、校验、拼接。FastAPI 本身不内置分块处理逻辑,必须靠手动解析请求体——关键不是用 File,而是读取原始 request.body() 或流式读取 request.stream()。
常见错误是直接用 UploadFile 接收,它会把整个请求体加载进内存,失去对单个 chunk 的控制权,且无法获取 Content-Range 头来定位偏移量。
- 必须从
request.headers提取Content-Range(如bytes 0-999/10000),用正则解析出start、end、total - 用
await request.body()读取当前 chunk 数据(适合中小 chunk,比如 ≤16MB);超大 chunk 建议用await request.stream()配合async for分批读取 - 文件存储路径需唯一绑定上传 ID(如前端传的
X-Upload-ID),避免不同文件写混
如何安全地拼接和校验上传中的文件块
拼接不是简单追加,要考虑并发写入冲突、重复上传、中间失败等现实问题。最稳妥方式是为每个上传 ID 维护一个临时目录,每块存为独立文件(如 chunk_0001.bin),全部收齐后再合并,而不是边收边写主文件。
校验不能只靠文件名或顺序,必须做内容级验证:前端应在每个 chunk 后附带 X-Chunk-Hash(如 SHA256),服务端收到后立即计算比对,不一致直接 400 返回。
立即学习“Python免费学习笔记(深入)”;
- 用
os.path.join(upload_dir, upload_id)隔离不同上传任务,防止路径穿越(务必校验upload_id只含字母数字) - 记录元数据到 JSON 文件(如
meta.json),包含已收 chunk 列表、总大小、状态(uploading/completed),便于断点恢复查询 - 合并时用
shutil.copyfileobj流式写入,避免把所有 chunk 加载进内存
怎么实现上传进度查询和断点恢复接口
客户端需要随时知道“还差哪几块”,所以必须提供基于 upload_id 的状态查询接口。这个接口不能只返回“成功/失败”,而要返回已接收的字节范围列表,让前端决定下一步该传哪块。
典型设计是 GET /upload/status/{upload_id},返回类似:{"uploaded_ranges": [[0, 999], [2000, 2999]], "total_size": 10000}。注意这个接口必须快——不要每次查都遍历目录,建议把 ranges 缓存在 Redis 或本地内存(用 LRUCache)。
- 避免在查询接口里做文件系统扫描,提前在接收 chunk 时更新内存或缓存中的 ranges 列表
- 上传 ID 必须全局唯一且不可预测(用
secrets.token_urlsafe(16)),防止恶意枚举他人上传状态 - 设置上传元数据过期时间(如 24 小时),避免磁盘被废弃临时文件占满
为什么不能依赖浏览器原生 fetch 的 Content-Range 自动处理
浏览器 fetch 不会自动拆分文件或填充 Content-Range,这完全是前端 JS 的责任。后端看到的 Content-Range 是前端手动 set 的 header,意味着:如果前端没正确计算偏移、没按顺序传、或传错 total,后端拼出来的文件就损坏。
所以服务端必须做防御性检查:收到 chunk 时,校验 start 是否 ≥0、end 是否 total、所有 range 是否互不重叠、累计长度是否等于 total。任意一项失败,立刻拒绝并返回明确错误码(如 416 Range Not Satisfiable)。
- 不要信任前端传的
total_size,最终合并后要用os.path.getsize()校验实际文件大小 - 同一
upload_id的多次上传请求必须串行化(用asyncio.Lock或 Redis 分布式锁),否则并发写同一 chunk 会导致数据错乱 - HTTP/2 下某些代理可能吞掉
Content-Range,建议同时要求前端在 body 开头放明文 offset 和 size 字段作 fallback


















