中文文件名下载乱码:RFC 5987 编码的解决原理
学完这篇,你能搞明白「下载下来的中文文件名变成 䏿–‡.zip」的根因,并且用 RFC 5987 的 filename* 参数在后端一次性修好,不用再靠猜浏览器。
第一步:先看清问题出在响应头
下载文件名不是网页正文的一部分,它藏在 HTTP 响应头里:
Content-Disposition: attachment; filename="中文报表.pdf"
而 HTTP 头字段按规定只能是 ASCII 字节。当你把 UTF-8 的中文直接塞进 filename 时,浏览器收到的其实是一串裸字节,各浏览器按不同编码去猜(有的按 ISO-8859-1,有的按本地编码),猜错就出现 䏿–‡ 这种典型乱码。
注意:浏览器下载栏里显示的名字已经是「解码错误后」的结果,光看它看不出原始字节长什么样,必须去看响应头本身。
第二步:用 curl 拿到真实响应头
在终端执行:
curl -sI -D - "https://你的域名/download.php?id=1" -o /dev/null
-D - 表示把响应头打到标准输出,-I 只发 HEAD 请求,-o /dev/null 丢掉正文。你也可以在 Chrome 里打开开发者工具 → Network → 点开这个下载请求 → Response Headers,效果一样。
第三步:理解 RFC 5987 的解法——写两个参数
RFC 5987 给 HTTP 头参数加了「扩展值」(ext-value)写法,RFC 6266 则规定了它在 Content-Disposition 里怎么用。核心就是同时给两个参数:
Content-Disposition: attachment; filename="download.pdf"; filename*=UTF-8''%E4%B8%AD%E6%96%87%E6%8A%A5%E8%A1%A8.pdf
filename:纯 ASCII 的兜底名,给不认识新参数的客户端用,可以直接写英文名或拼音。filename*:格式固定为字符集'语言'百分号编码值。语言通常留空,所以是UTF-8''(注意是两个单引号连写),后面跟 UTF-8 字节的 percent-encoding。
浏览器遇到 filename* 时优先用它,解码后就是正确的中文。
注意:
filename*的值不能加双引号。写成filename*="UTF-8''%E4%B8%AD..."会让部分浏览器直接忽略整个参数,这是最常见的「改了还是乱码」的原因。
第四步:PHP 里怎么生成(别用错编码函数)
$name = '年度报表 2024.pdf';
$ascii = 'report-2024.pdf'; // 纯 ASCII 兜底名
$enc = rawurlencode($name); // 按 UTF-8 字节做百分号编码
header('Content-Type: application/octet-stream');
header("Content-Disposition: attachment; filename=\"{$ascii}\"; filename*=UTF-8''{$enc}");
header('Content-Length: ' . filesize($path));
readfile($path);
关键点:
- 必须用
rawurlencode,不能用urlencode。 后者会把空格变成+,而+在 URL 编码里只在查询串中代表空格,用在文件名里会被原样解码成加号。 - RFC 5987 规定只有非 attr-char 才需要编码(attr-char 包括字母、数字和
!#$&+-.^_\|~)。rawurlencode会把~` 之类也编掉,属于「多编了」,但百分号编码解码是无损的,浏览器照样还原正确,可以放心用。 - 两个参数都要写,且
filename在前、filename*在后。
第五步:验证 + 几个高频坑
验证方法:改完再用第二步的 curl 看一眼头,然后用一个文件名里同时含中文、空格、括号的压缩包实测下载。
注意:如果响应头正确但下载仍然乱码,先怀疑中间层——Nginx、CDN、反向代理可能重写了
Content-Disposition并丢掉filename*。可以直连后端端口对比一次,确认是不是代理的问题。
另外两个别踩的坑:
- 不要用
mb_convert_encoding把文件名转成 GBK 塞进filename。 这在 Windows + 中文环境偶尔能蒙对,到了 macOS、Linux 或 Firefox 就继续乱,属于用错误换兼容。 - 老 IE 不支持
filename*,但今天基本可以不管;如果确实有需求,兜底方案是把filename也写成 URL 编码形式,而不是写 GBK 字节。
小结
- 根因:HTTP 头只能是 ASCII,中文直接塞
filename必然被浏览器猜错编码。 - 正解:
filename="英文兜底名"; filename*=UTF-8''<百分号编码>,两个都写。 - 编码函数用
rawurlencode,filename*的值不加引号,UTF-8''是两个单引号。 - 排查顺序看响应头 → 看中间层是否改写 → 再怀疑代码。
- 别用 GBK 硬转,也别靠
urlencode。
转载请注明出处,版权归原作者所有。
星耀SVIP
管理员
黑卡会员






