PHP上传中文文件名报错主因是RFC 7578要求filename必须为ASCII,浏览器编码不一导致$_FILES'file'乱码或为空;正确做法是前端用encodeURIComponent()传原始名至额外字段,后端urldecode()还原并校验。

PHP 上传中文文件名报错的典型现象
常见错误不是 PHP 自身抛出,而是浏览器或 Web 服务器(如 Nginx/Apache)在解析 Content-Disposition 头时失败,导致 $_FILES 数组为空、$_FILES['file']['name'] 为乱码或空字符串,甚至返回 400 Bad Request。根本原因在于 RFC 7578 明确要求 multipart 表单中 filename 参数必须是 ASCII 字符,中文直接传入属于协议违规。
浏览器行为差异明显:Chrome 和 Firefox 会自动对中文 filename 做 ISO-8859-1 编码(非 URL 编码),而 Safari 可能直接拒绝提交;服务端若未适配,$_FILES 就拿不到原始信息。
不要对 $_FILES['file']['name'] 做 URL 解码
很多开发者看到 $_FILES['file']['name'] 是乱码(如 %E4%BD%A0%E5%A5%BD.txt),第一反应是用 urldecode(),这是典型误区——这个值从来就不是 URL 编码过的,它是浏览器按 multipart 规范“伪装”成 ASCII 的原始字节序列,解码只会让问题更糟。
-
urldecode() 对 $_FILES['file']['name'] 无效,且可能破坏二进制数据
- 真正需要处理的是 HTTP 请求头中的原始
filename 字段,但 PHP 默认不暴露它
- PHP 8.1+ 提供了
$_FILES['file']['orig_name'](仅限某些 SAPI),但不可靠,不建议依赖
可靠方案:前端用 encodeURIComponent + 后端读取原始 header
最稳的做法是绕过 $_FILES['file']['name'],改由前端主动把原始中文文件名做 encodeURIComponent(),通过额外字段传过来,后端校验并还原:
<input type="file" id="file" />
<script>
document.getElementById('file').onchange = function() {
const file = this.files[0];
const nameField = document.createElement('input');
nameField.type = 'hidden';
nameField.name = 'orig_filename';
nameField.value = encodeURIComponent(file.name); // 如 "你好.txt" → "%E4%BD%A0%E5%A5%BD.txt"
this.form.appendChild(nameField);
};
</script>
urldecode() 对 $_FILES['file']['name'] 无效,且可能破坏二进制数据filename 字段,但 PHP 默认不暴露它$_FILES['file']['orig_name'](仅限某些 SAPI),但不可靠,不建议依赖$_FILES['file']['name'],改由前端主动把原始中文文件名做 encodeURIComponent(),通过额外字段传过来,后端校验并还原:
<input type="file" id="file" />
<script>
document.getElementById('file').onchange = function() {
const file = this.files[0];
const nameField = document.createElement('input');
nameField.type = 'hidden';
nameField.name = 'orig_filename';
nameField.value = encodeURIComponent(file.name); // 如 "你好.txt" → "%E4%BD%A0%E5%A5%BD.txt"
this.form.appendChild(nameField);
};
</script>
后端接收时:
- 用
urldecode($_POST['orig_filename'])还原中文名,注意检查是否为空或非法字符 - 忽略
$_FILES['file']['name'],它只是兼容占位符,不可信 - 保存文件时,用还原后的名字拼路径,例如:
$safeName = basename(urldecode($_POST['orig_filename']));
Nginx/Apache 配置层面的补充注意点
即使前端做了处理,如果 Web 服务器对非 ASCII header 过于严格,仍可能拦截请求。需确认:
- Nginx:确保没有启用
underscores_in_headers on; 或其他 header 过滤规则,它不影响 filename,但可能干扰自定义字段
- Apache:默认接受 UTF-8 header,但若用了
mod_security,需检查规则是否误杀含 % 的 orig_filename 值
- 所有环境都应设置
default_charset = "UTF-8"(php.ini),避免内部字符串处理出错
实际中最容易被忽略的,是前端没做 encodeURIComponent 而直接赋值给 hidden input,导致空格、括号等字符破坏表单结构;或者后端没过滤 ../ 类路径遍历,把用户传来的恶意文件名直接拼进 fopen()。
underscores_in_headers on; 或其他 header 过滤规则,它不影响 filename,但可能干扰自定义字段mod_security,需检查规则是否误杀含 % 的 orig_filename 值default_charset = "UTF-8"(php.ini),避免内部字符串处理出错



















