bigywater.tech
Flask · 43

send_file() 与 send_from_directory()

提供文件下载该用哪个函数?响应头里有什么?中文文件名怎么处理?路径穿越被挡住了吗?大文件会不会撑爆内存?这篇用 test client 与真实 gunicorn 逐项验证。

35 vs 335 MiB
300 MB 下载:send_file vs open().read() 的 worker 峰值内存
404 / 500
文件不存在时:send_from_directory / send_file 的状态码
filename*
带中文的 download_name 在 Content-Disposition 里的完整编码形式

文件的响应头

实测 · 本机执行输出
  GET /file   → 200 Content-Type: text/plain; charset=utf-8 | Content-Length: 17 | Content-Disposition: inline; filename=report.txt | Cache-Control: no-cache | ETag: "1790911494.4854403-17-820325724" | Last-Modified: Fri, 02 Oct 2026 03:24:54 GMT
  GET /abs    → 200 Content-Type: text/plain; charset=utf-8 | Content-Length: 17 | Content-Disposition: inline; filename=report.txt | Cache-Control: no-cache | ETag: "1790911494.4854403-17-820325724" | Last-Modified: Fri, 02 Oct 2026 03:24:54 GMT
  GET /dl     → 200 Content-Type: text/plain; charset=utf-8 | Content-Length: 17 | Content-Disposition: attachment; filename=report.txt | Cache-Control: no-cache | ETag: "1790911494.4854403-17-820325724" | Last-Modified: Fri, 02 Oct 2026 03:24:54 GMT
  GET /dl-cn  → 200 Content-Type: text/plain; charset=utf-8 | Content-Length: 17 | Content-Disposition: attachment; filename=" (1).txt"; filename*=UTF-8''%E6%9C%88%E5%BA%A6%20%E6%8A%A5%E5%91%8A%281%29.txt | Cache-Control: no-cache | ETag: "1790911494.4854403-17-820325724" | Last-Modified: Fri, 02 Oct 2026 03:24:54 GMT

上一篇:Streaming Response;下一篇:ETag 与条件请求;路径穿越专题:路径穿越与文件下载。

内存里的文件

实测 · 本机执行输出
  GET /mem      → 200 Content-Type: text/plain; charset=utf-8 | Content-Disposition: inline; filename=m.txt | Content-Length: 9
  GET /mem-bad  → 500 Content-Type: text/html; charset=utf-8 | Content-Length: 265
  GET /mem-name → 200 Content-Type: text/csv; charset=utf-8 | Content-Disposition: inline; filename=export.csv | Content-Length: 8
  直接调用 /mem-bad 的视图 → TypeError: Unable to detect the MIME type because a file name is not available. Either set 'download_name', pass a path instead of a file, or set 'mimetype'.

缓存参数

实测 · 本机执行输出
  GET /age      → 200 Cache-Control: public, max-age=3600 | ETag: "1790911494.4854403-17-820325724" | Last-Modified: Fri, 02 Oct 2026 03:24:54 GMT | Expires: Fri, 02 Oct 2026 04:24:54 GMT
  GET /nocache  → 200 Cache-Control: no-cache, max-age=0 | Last-Modified: Fri, 02 Oct 2026 03:24:54 GMT | Expires: Fri, 02 Oct 2026 03:24:54 GMT
  GET /nocond   → 200 Cache-Control: no-cache | ETag: "1790911494.4854403-17-820325724" | Last-Modified: Fri, 02 Oct 2026 03:24:54 GMT
  带 If-None-Match → 304 | 正文 0 字节
  conditional=False 的路由带 If-None-Match → 200

send_from_directory

实测 · 本机执行输出
  GET /dir/report.txt              → 200
  GET /dir/../secret.txt           → 404
  GET /dir/..%2fsecret.txt         → 404
  GET /dir/%2e%2e/secret.txt       → 404
  GET /dir//etc/passwd             → 308
  GET /dir/sub/../report.txt       → 200
  GET /dir/nope.txt                → 404
  GET /dir-rel/report.txt          → 200
  找不到文件 send_file("data/nope.txt") → 视图里抛出 FileNotFoundError → 经过 HTTP 是 500
  找不到文件 send_from_directory(目录, "nope.txt") → NotFound → HTTP 404

Content-Type

实测 · 本机执行输出
  report.txt       → text/plain; charset=utf-8
  pic.png          → image/png
  page.html        → text/html; charset=utf-8
  note.md          → text/markdown; charset=utf-8
  data.json        → application/json
  archive.tar.gz   → application/x-tar
  noext            → application/octet-stream
  script.js        → text/javascript; charset=utf-8
  覆盖 mimetype="application/x-custom" → application/x-custom

X-Sendfile

实测 · 本机执行输出
  USE_X_SENDFILE=True → 状态 200 | X-Sendfile = <项目>/data/report.txt | 正文长度 0

大文件的内存

实测 · 本机执行输出
  gunicorn 下 wsgi.file_wrapper = {"file_wrapper":"<class 'gunicorn.http.wsgi.FileWrapper'>"}
  send_file                峰值 RSS ≈ 35 MiB(基线 35 MiB)| curl:314572800 字节 0.065262s
  open().read() 整个读进内存     峰值 RSS ≈ 335 MiB(基线 35 MiB)| curl:314572800 字节 0.105458s

分步演示

send_file
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动
    • 没有验证:X-Sendfile 在 Apache(mod_xsendfile)与 Nginx(X-Accel-Redirect)上的真实效果、Windows 下的路径写法、gunicorn 的 sendfile 系统调用是否被使用(只验证了 file_wrapper 的类型和内存)、Range 请求(见第 49 篇)
    京ICP备2026067502号-1京公网安备11010502063487号