上传一个30MB的Excel,页面转半天才返回,PHP里$_FILES是空数组,error_log里连一条记录都没有。这种静默失败最难查,因为服务端什么都不报。
碰上这个,八成是三层配置里有一层没放开:PHP自己一层,Web服务器(nginx或Apache)一层,前面挂的反向代理或CDN还有一层。任一层把请求拦下,现象都一样,前端像卡住,后端收不到文件。
先分清是哪种失败
别急着翻配置,先把$_FILES['file']['error']打出来。它返回的是整数错误码,不是字符串:
1 => UPLOAD_ERR_INI_SIZE 文件超过upload_max_filesize 2 => UPLOAD_ERR_FORM_SIZE 文件超过表单里的MAX_FILE_SIZE 3 => UPLOAD_ERR_PARTIAL 只传了一部分 4 => UPLOAD_ERR_NO_FILE 没选文件 6 => UPLOAD_ERR_NO_TMP_DIR 临时目录不存在 7 => UPLOAD_ERR_CANT_WRITE 临时目录写不进去 8 => UPLOAD_ERR_EXTENSION 被扩展拦下
传大文件返回1,是php.ini挡的。返回3,传到一半断了,去看Web服务器的超时和缓冲区。要是$_FILES整个是空数组,连error字段都摸不到,那基本不是PHP的锅,请求体在更外层就被拒了。
php.ini里要动的几个值
默认值小得离谱:upload_max_filesize是2M,post_max_size是8M。PHP的5.2、7.4、8.2、8.4,这几个数一直没动过。
file_uploads = On upload_max_filesize = 50M post_max_size = 60M max_file_uploads = 20 memory_limit = 256M max_execution_time = 300 max_input_time = 300 upload_tmp_dir = /var/tmp
post_max_size必须比upload_max_filesize大。这条最容易忽略:请求体是multipart编码,文件之外还有boundary、字段名、其它表单项,实际体积比文件本身大出百分之几。要是post_max_size设得比upload_max_filesize还小,PHP会把整个请求体丢掉,$_FILES和$_POST同时变空,而且不写任何日志。当初就是在这儿栽的,查了两个钟头才发现两个值写反了。
upload_tmp_dir指向的目录要存在且可写,/var/tmp被systemd-tmpfiles清掉的机器上出过一次7号错误。实测放到/var/spool/php-upload这种专属目录更稳。
改对了不一定生效
php.ini不止一份。命令行跑php --ini看到的,和php-fpm加载的往往不是同一个文件。Debian系通常是/etc/php/8.2/fpm/php.ini,CLI那边是/etc/php/8.2/cli/php.ini,改错了地方等于没改。
php --ini
php -r 'var_dump(ini_get("upload_max_filesize"), ini_get("post_max_size"));'
上面这条只能看CLI的值。要确认Web那侧真正生效的数字,临时建个info.php,看完立刻删:
echo '<?php phpinfo();' > /var/www/html/info.php curl -s http://127.0.0.1/info.php | grep -i upload_max rm -f /var/www/html/info.php
改完必须重载,漏了这一步前面全白改。
nginx -t && systemctl reload nginx systemctl restart php8.2-fpm systemctl is-active php8.2-fpm
nginx那一层的413
nginx的client_max_body_size默认是1m。文件超过它,nginx直接回413 Request Entity Too Large,请求压根不会交给PHP,所以$_FILES必然是空的。浏览器Network面板里能看到状态码413,这是最快的判断依据。
http {
client_max_body_size 50m;
client_body_buffer_size 1m;
client_body_temp_path /var/cache/nginx/client_temp;
}
写在http块里全局生效,也可以只写在某个server或location里。nginx的1.24.0、1.25.3、1.27.1,这几个版本的写法和行为一致,不用区分。
错误日志里对应的记录长这样,看到它就别再翻PHP配置了:
*12345 client intended to send too large body: 31457280 bytes, client: 10.0.0.7
换成Apache是LimitRequestBody指令,默认0表示不限制,单位是字节:
LimitRequestBody 52428800
顺带提一句,client_body_temp_path所在分区满了也会失败,症状和413不同,日志里是No space left on device。
反向代理和CDN还有一层
请求到源站之前,前面可能还挡着东西。Cloudflare免费版对单个文件上传卡在100MB,超过直接回413;各家云负载均衡的七层监听器也有自己的body上限。这类限制的特点是源站nginx和PHP的日志干干净净,一行都没有。碰到过一次,改了两天配置,最后发现是前面那层挡的。
判断方法很直接,绕过前面那层打源站对比:
curl -s -o /dev/null -w "%{http_code}\n" -F "file=@big.xlsx" http://10.0.0.7/upload.php
curl -s -o /dev/null -w "%{http_code}\n" -F "file=@big.xlsx" https://www.example.com/upload.php
两条都返回200就都通了;源站200、带CDN那个413,问题位置立刻就清楚了。
前端先挡一道更省事
让用户等30MB传完才提示失败,体验很差。提交之前用File.size比一下就行:
const f = document.querySelector('#file').files[0];
if (f && f.size > 50 * 1024 * 1024) {
alert('文件超过50MB,请压缩后再传');
return;
}
把限制一路放大不是真正的出路。500MB的文件走一次POST,中间网络抖一下就全没了,PHP还得占着一个fpm进程等到传完。分片上传,或者让前端直传对象存储、后端只收一个key,是更稳的做法。
上面这些默认值与参数名核实到2026年9月,PHP 8.4与nginx 1.27.1上均适用。