中文文件名下载乱码:RFC 5987 编码的解决原理

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP星耀SVIP管理员管理员 黑卡会员黑卡会员
发布于 2026-10-05 20:00 ·5 浏览 ·0 回复

学完这篇,你能搞明白「下载下来的中文文件名变成 䏿–‡.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);

关键点:

  1. 必须用 rawurlencode,不能用 urlencode。 后者会把空格变成 +,而 + 在 URL 编码里只在查询串中代表空格,用在文件名里会被原样解码成加号。
  2. RFC 5987 规定只有非 attr-char 才需要编码(attr-char 包括字母、数字和 !#$&+-.^_\|~)。rawurlencode 会把 ~` 之类也编掉,属于「多编了」,但百分号编码解码是无损的,浏览器照样还原正确,可以放心用。
  3. 两个参数都要写,且 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。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-712.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~