核心问题是content-disposition的filename参数不支持utf-8原生编码,现代浏览器要求rfc 5987规范的filename*=utf-8''格式,需用rawurlencode()编码并清除bom及控制符,同时提供ascii fallback。

PHP 7.4 下载服务器文件时中文名乱码,核心问题不是 PHP 版本,而是 Content-Disposition 头部的 filename 编码方式不被浏览器正确识别——尤其在 Chrome、Edge(Chromium 内核)和新版 Firefox 中,仅用 urlencode() 已不够稳定。
为什么 urlencode() 在 PHP 7.4+ 下经常失效
旧写法 header("Content-Disposition: attachment; filename=" . urlencode($filename)) 生成的是 RFC 2231 不兼容格式,浏览器会把 %E4%B8%AD%E6%96%87.txt 当作 ASCII 字符串直接显示,而非解码为中文。PHP 7.4 默认更严格遵循 HTTP 标准,且现代浏览器已基本放弃对这种“伪兼容”方式的支持。
- Chrome 85+、Firefox 77+、Edge 88+ 均要求显式声明
filename*=参数 -
urlencode()会把空格转成+,而filename*=要求使用%20(rawurlencode()才满足) - 如果原始字符串含 UTF-8 BOM 或混合编码(如 GBK 来源),
urlencode()不做校验,直接编码脏数据
filename*=UTF-8'' 是唯一可靠方案
必须同时提供传统 filename(兼容老 IE)和标准 filename*=(现代浏览器主力识别)。PHP 7.4+ 可安全使用 rawurlencode() 配合手动拼接:
$filename = '报告_2026年Q3.pdf';
$encoded = rawurlencode($filename);
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $filename . '"; filename*=UTF-8\'\'' . $encoded);
header('Content-Length: ' . filesize($filepath));
readfile($filepath);
exit;
-
filename="..."是 fallback,IE 11 及更早版本依赖它(即使显示为乱码,至少能下载) -
filename*=UTF-8''...中的两个单引号之间**不能有空格**,否则 Safari 会忽略 -
rawurlencode()确保空格→%20、中文→%E4%B8%AD,符合 RFC 5987 - 不要用
mb_convert_encoding($filename, 'UTF-8', 'auto')替代检测——若输入已是 UTF-8,二次转换可能引入损坏
当文件名来自数据库或表单时,先清理再编码
用户上传或数据库读出的文件名常带不可见控制字符(如零宽空格、BOM、CRLF),直接 rawurlencode() 会导致下载失败或名称截断:
- 用
trim($filename)去首尾空白 - 用
preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $filename)清除非法控制符 - 确认来源编码:若从 GBK 表单来,需
iconv('GBK', 'UTF-8//IGNORE', $filename)转换后再rawurlencode() - 避免用
mb_detect_encoding()自动探测——它在多字节混合时极易误判,宁可明确指定来源编码
容易被忽略的底层陷阱
即使 header 写对了,仍乱码,大概率是以下任一环节污染了输出流:
- 被
include或require的配置文件含 UTF-8 BOM(检查hexdump -C config.php | head -n1是否以ef bb bf开头) - PHP 文件本身保存为 “UTF-8 with BOM”,导致
header()前已有隐式输出 - 启用
output_buffering后未清空缓冲区:ob_end_clean();必须在header()前调用 - Apache 的
AddDefaultCharset或 Nginx 的charset指令强制注入了冲突的 charset,覆盖了你设的Content-Type
真正棘手的从来不是怎么写 header,而是确保 header 能干净发出——BOM 和前置空行,在 PHP 7.4+ 下几乎 100% 触发 “Headers already sent” 隐性失败,让整个下载逻辑降级为普通文本输出,中文名自然崩坏。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











