fastapi 如何实现 StreamingResponse 的分块传输大文件

11次阅读

streamingResponse适合大文件传输,因其采用http分块编码边读边发,避免内存溢出和延迟;需用生成器逐块yield字节流,禁用nginx缓冲并设置正确headers。

fastapi 如何实现 StreamingResponse 的分块传输大文件

StreamingResponse 为什么适合大文件传输

因为 StreamingResponse 不会把整个文件读进内存,而是边读边发,避免 OOM 和响应延迟。它底层用的是 HTTP chunked transfer encoding,客户端(比如浏览器curl)能边收边处理,对视频、日志、导出 csv 等场景很实用。

但注意:fastapi 默认不启用 gzip 压缩,且中间件(如 GZipMiddleware)可能干扰分块;Nginx 等反向代理默认会缓冲响应,必须显式关闭缓冲才能看到实时分块效果。

如何正确构造 StreamingResponse 返回文件流

核心是传一个可迭代对象(如生成器),每次 yield 一个 bytes 块。不能用 open(...).read(),否则全量加载就失去流的意义。

  • open(file_path, "rb") 配合 .read(chunk_size) 循环 yield,推荐 chunk_size=8192(8KB)——太小增加 syscall 开销,太大削弱“流感”
  • 必须设置 media_type(如 "application/octet-stream"),否则浏览器可能无法识别下载行为
  • 建议加 headers={"Content-Disposition": 'attachment; filename="xxx.bin"'} 触发下载而非内嵌预览
  • 别在生成器里做耗时操作(如数据库查询、网络请求),否则阻塞整个流
from fastapi import FastAPI from fastapi.responses import StreamingResponse  app = FastAPI()  def file_stream(path: str):     with open(path, "rb") as f:         while chunk := f.read(8192):             yield chunk  @app.get("/download") def download_file():     return StreamingResponse(         file_stream("/path/to/big.zip"),         media_type="application/zip",         headers={"Content-Disposition": 'attachment; filename="big.zip"'}     )

为什么 Nginx 会吞掉 chunk,怎么破

默认配置下,Nginx 会等整个响应结束才转发给客户端,导致“卡住几秒后突然下载完成”。这不是 FastAPI 的问题,而是反向代理的缓冲策略。

  • location 块中加 proxy_buffering off;
  • 同时禁用缓存相关头:proxy_cache off;proxy_http_version 1.1;chunked_transfer_encoding on;
  • 如果用了 proxy_redirectproxy_set_header,确保没覆盖 Transfer-Encoding

验证是否生效:用 curl -v http://your-domain/download,看响应头是否有 Transfer-Encoding: chunked,且 body 是分段打印的(不是一次性吐完)。

异步文件读取能否提升性能

不能直接用 asyncio.open()(标准库不支持),但可用 anyio.Pathaiopath 实现真正异步 IO。不过对单个大文件流来说,同步 read() + yield 已足够——瓶颈通常在磁盘或网络,不是 python 线程阻塞。

真正需要异步的场景是:多个并发流共享同一文件句柄、或需在读取过程中穿插其他 await 操作(如权限校验、审计日志)。这时建议用 starlette.background.BackgroundTasks 或拆成独立任务,而不是强行套 async def + 同步 open

容易忽略的一点:如果文件路径来自用户输入,务必做路径净化(如 pathlib.Path(file_param).resolve().relative_to(allowed_root)),否则 ../ 可能导致任意文件读取。

text=ZqhQzanResources