PHP上传大文件失败返回413或$_FILES为空的解决方法(upload_max_filesize与nginx client_max_body_size配置)

上传一个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上均适用。