根本原因是HTTP Content-Disposition头的filename参数不支持UTF-8原生编码,且浏览器对filename解析兼容性差;正确做法是按RFC 5987规范同时提供ASCII fallback filename和UTF-8编码的filename,并确保原始文件名是UTF-8、无BOM、环境不污染响应头。

ThinkPHP 文件响应(如下载、导出)返回中文文件名时乱码,根本原因是 HTTP Content-Disposition 头中的 filename 参数不支持 UTF-8 原生编码,而不同浏览器对 filename* 的解析兼容性又不一致。直接拼接中文到 filename="xxx" 会触发 GBK/ISO-8859-1 回退,导致乱码或截断。
Content-Disposition 中文 filename 不生效的典型表现
常见错误现象包括:
- Chrome 下显示为
%E4%B8%AD%E6%96%87.pdf(URL 编码但未解码) - Firefox 或 Edge 下文件名变成空、下划线或问号
- IE/Edge Legacy 直接报错“无法保存文件”
- 响应头里同时写了
filename和filename*,但只有其中一个被识别
ThinkPHP 6+ 正确设置中文文件名的写法
TP6 使用 Response::create()->header() 或 response()->download() 时,不能靠手动拼接 header 字符串。必须按 RFC 5987 规范构造 filename*,并兼容 fallback filename。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 用
iconv('UTF-8', 'ASCII//TRANSLIT', $name)生成 ASCII 兼容 fallback 名(如zhongwen.pdf),避免纯乱码 - 用
rawurlencode()对原始中文名做 UTF-8 编码,再拼成filename*=UTF-8''{encoded} - 务必确保原始文件名变量是 UTF-8 编码(不是 GBK,也不含 BOM)
- 不要调用
mb_convert_encoding($name, 'UTF-8', 'auto')—— auto 检测不可靠,可能把 UTF-8 当 GBK 再转一次
示例(TP6):
$name = '报表_2026年9月.xlsx';
$encoded = rawurlencode($name);
$asciiFallback = iconv('UTF-8', 'ASCII//TRANSLIT', $name) ?: 'file.xlsx';
<p>return response()->download($filePath)
->header('Content-Disposition', "attachment; filename=\"{$asciiFallback}\"; filename*=UTF-8''{$encoded}");
为什么 setSaveName() 或 move() 里的 iconv 转码不管用
这个操作只影响服务器端保存的文件路径,和 HTTP 响应头里的文件名完全无关。很多开发者误以为“上传时转了 GBK → UTF-8”,下载时就能自动还原,其实:
-
File->move()的目标路径是服务端磁盘路径,操作系统(Linux/Windows)对中文路径的支持取决于 locale,与 HTTP 无关 -
setSaveName()修改的是保存后的文件名字符串,不会注入到Content-Disposition头中 - 即使磁盘上存的是
报表_2026年9月.xlsx,response()->download()默认仍用原始请求参数或硬编码名,不读取磁盘真实名
容易被忽略的底层陷阱
最隐蔽的问题往往不在 PHP 层,而在环境链路:
- Apache 的
AddDefaultCharset若设为GBK,会覆盖 PHP 的 header,强制所有响应带charset=gbk,干扰filename*解析 - Nginx 反向代理时若配置了
underscores_in_headers on,可能丢弃含星号的filename*头(因 * 是非法 header 字符) - PHP-FPM 的
catch_workers_output = yes开启后,worker 错误日志可能被输出到响应体开头,污染 header,导致headers already sent,进而使Content-Disposition失效 - ThinkPHP 中间件(如日志、调试)若提前 echo 任何内容(包括空格、换行、BOM),同样会破坏 header 发送
验证方式:用 curl -I http://your.site/export 直接看响应头,确认 Content-Disposition 是否完整出现且无截断 —— 这比在浏览器里点下载更可靠。



















