django 用 escape_uri_path,flask 用 urllib.parse.quote,纯 http 响应手动构造时必须区分 rfc 5987 和 rfc 2231 语法。因为 filename 不支持非 ascii,需用 filename*(rfc 5987)并严格格式化,escape_uri_path 输出合法 utf-8'' 编码,而 quote 缺少前缀导致浏览器无法识别。

escape_uri_path,Flask 用 urllib.parse.quote,纯 HTTP 响应手动构造时必须区分 RFC 5987 和 RFC 2231 语法**。其他“encode 再拼字符串”的做法基本都失效。
为什么 response['Content-Disposition'] = 'attachment; filename=中文.txt' 会乱码或下载失败
浏览器对 Content-Disposition 中的 filename 参数只接受 ASCII 字符。直接塞中文进去,不同浏览器解析方式完全不同:Chrome 可能截断、Firefox 可能拒收、Safari 可能显示为 %E4%B8%AD%E6%96%87.txt。这不是 Python 编码问题,而是 HTTP 协议规范限制。
关键点在于:filename 是旧式字段(RFC 2616),不支持非 ASCII;而 filename* 才是带编码的现代字段(RFC 5987),但必须严格按格式写,否则被忽略。
- 常见错误:用
'测试'.encode('utf-8')拼进filename=—— 这只是字节串转成 bytes 对象,不是合法的 URI 编码,HTTP 头里出现裸二进制会破坏协议 - 另一个坑:Django 1.11+ 默认禁用
filename*,除非显式启用或用兼容函数 - 注意响应头顺序:某些 Nginx/CDN 会重写或丢弃重复的
Content-Disposition,确保只设一次
Django 下必须用 escape_uri_path 而非 quote
Django 自带的 django.utils.encoding.escape_uri_path 是专为 filename* 设计的:它输出形如 filename*=utf-8''%E4%B8%AD%E6%96%87.txt 的合法 RFC 5987 字段,且自动处理单引号、空格等边界字符。
别用 urllib.parse.quote 替代——它默认不加 utf-8'' 前缀,浏览器无法识别编码意图。
- 正确写法:
from django.utils.encoding import escape_uri_path<br>response['Content-Disposition'] = f"attachment; filename*=UTF-8''{escape_uri_path(filename)}" - 如果 filename 已含扩展名(如
"报告.xlsx"),escape_uri_path会原样保留点号和扩展名,无需额外处理 - 旧版 Django(filename*,只能降级用
filename+ GBK 编码(仅限 IE/老内网环境),不推荐
Flask 或原生 WSGI 应用要用 urllib.parse.quote 并手动拼 filename*
Flask 没有内置等价函数,必须自己构造 RFC 5987 格式。核心是:编码后加 utf-8'' 前缀,且使用 safe='' 避免对斜杠等字符二次编码。
- 正确写法:
from urllib.parse import quote<br>encoded_name = quote(filename.encode('utf-8'), safe='')<br>response.headers['Content-Disposition'] = f"attachment; filename*=utf-8''{encoded_name}" - 不能漏掉
safe='':否则quote会对/、.等也编码,导致扩展名丢失 - 注意大小写:
utf-8小写,''是两个单引号,中间无空格 - 若需兼容极老客户端(如 Windows XP 上的 IE6),可同时提供
filename和filename*,浏览器优先选后者
前端发起下载时,download 属性对中文的支持极不可靠
不要依赖 <a download="中文名"></a> —— Safari 完全忽略,Chrome 在跨域或 Blob 场景下静默失效,Firefox 对 UTF-8 名称支持不稳定。
真正可控的方式是:后端返回正确 Content-Disposition,前端只负责触发 fetch + Blob + URL.createObjectURL,把命名权完全交给后端。
- 前端只需:
fetch('/api/download')<br> .then(r => r.blob())<br> .then(blob => {<br> const url = URL.createObjectURL(blob);<br> const a = document.createElement('a');<br> a.href = url;<br> // 这里填的 download 值会被后端 header 覆盖,可任意写<br> a.download = 'fallback-name.txt';<br> a.click();<br> URL.revokeObjectURL(url);<br> }); - 如果后端 header 正确,
a.download的值实际不起作用;但如果后端没设好,它最多 fallback 到英文名,不会乱码 - 切记:Blob 下载无法绕过 CORS,服务端必须带
Access-Control-Allow-Origin,否则 fetch 失败
filename 和 filename* 的协议地位——前者是摆设,后者才是唯一可靠路径。只要 header 里出现合法的 filename*=...,所有现代浏览器都会按 UTF-8 解码并显示正确中文名。Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











