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