413 Request Entity Too Large:每个站长迟早撞上的墙
网站上线一段时间后,几乎都会遇到同一个报错:用户上传一张稍微大点的图,或者后台要导入一个几十兆的 CSV,页面直接返回「413 Request Entity Too Large」。这是 Nginx 的默认限制在拦你——client_max_body_size 默认只有 1M。对做内容站的人来说,不能传大文件意味着投稿、素材、备份导入全被卡住。
但把这个值简单调大只是第一步,真正麻烦的是 413 往往牵连四五个环节:Nginx 前端、后端 PHP/应用、反向代理链路、CDN 边缘节点,还有系统层的超时。任何一层没放开,用户看到的还是失败。本文按个人站长上传大文件的完整链路,把 413 的定位与解决、上传限速、分片上传和断点续传讲清楚,命令在 Nginx 1.24 + PHP-FPM + Debian 12 上实测过。
先定位:413 到底是谁返回的
不要一上来就改配置。先用 curl 看清响应头里的 Server 字段,确认是哪一层在拦:
curl -I -X POST --data-binary @bigfile.zip \
https://www.example.com/upload.php如果 Server 是 nginx,说明是 Nginx 层的 client_max_body_size 在拦;如果 Server 是 cloudflare 或返回带有 CF-Ray 头,那可能是 CDN 边缘的请求体限制(Cloudflare 免费版单请求上限 100M),改后端根本没用。分清责任层,才不会白改一通。
Nginx 层:正确设置 client_max_body_size
确定是 Nginx 拦的,就在站点配置里放开。注意这个指令可以放在 http、server、location 三个层级,就近覆盖:
server {
listen 443 ssl;
server_name www.example.com;
# 全局放开到 64M
client_max_body_size 64M;
location /upload.php {
# 只对上传接口单独放宽,避免全站被大包拖累
client_max_body_size 256M;
}
}一个常被忽略的细节:如果你前面还有一层 Nginx 做反向代理,那么代理层的 client_max_body_size 和 proxy_request_buffering 都要一起看。默认 proxy_request_buffering on 会把整个请求体先缓冲到磁盘再转发,大文件会先占满代理层的 /var/cache 空间。文件特别大时,改成 proxy_request_buffering off 让 Nginx 边收边转,能显著降低代理层的磁盘压力:
location / {
proxy_pass http://127.0.0.1:9000;
proxy_request_buffering off;
client_max_body_size 256M;
}改完务必 nginx -t 检查语法再 reload,语法错会导致 reload 直接失败、老进程继续跑旧配置,你以为生效了其实没有。
PHP 层:三处限制一个都不能漏
Nginx 放开了,PHP 还可能拦。PHP 有三个和上传相关的限制,必须同时大于你要传的大小,取最小值生效:
; /etc/php/8.2/fpm/php.ini
upload_max_filesize = 256M
post_max_size = 256M
max_execution_time = 300
memory_limit = 512M- upload_max_filesize:单个文件的上限。
- post_max_size:整个 POST 请求体的上限,必须 ≥ upload_max_filesize,否则多文件上传会莫名其妙失败。
- max_execution_time:大文件写入慢,默认 30 秒常被超时中断,调到 300 秒更稳。
改 php.ini 后要重启 php-fpm(reload 有时不重读 php.ini):systemctl restart php8.2-fpm。验证是否生效,别靠猜,写个 phpinfo 页面或命令行查:
php -i | grep -E "upload_max_filesize|post_max_size|memory_limit"上传限速:放开大小的同时别把自己拖垮
无限制的大文件上传是带宽杀手。个人 VPS 带宽本来就窄,几个用户同时传几百兆,整站访问都会被拖慢。Nginx 提供了两个限速指令,可以既允许大文件又能管住带宽:
# 定义一个限速区,按 IP 限速,10M 内存约能记 16 万个 IP
limit_req_zone $binary_remote_addr zone=upload:10m rate=2r/s;
location /upload.php {
client_max_body_size 256M;
limit_rate 2m; # 每个连接限速 2MB/s
limit_rate_after 5m; # 前 5MB 不限速,快速开始,之后限速
limit_req zone=upload burst=5 nodelay;
}limit_rate_after 是个好用的折中:小文件几乎不受影响,只有当上传超过阈值才开始限速,既保证体验又防滥用。若被限速的请求超时,还要同步调大 client_body_timeout,否则慢速上传到一半就被 Nginx 掐断:
client_body_timeout 120s;
send_timeout 120s;大文件上传的正解:分片 + 断点续传
对真正的大文件(几百兆到几个 G),把限制一路调大并不是好办法——单个请求越久越容易断,一旦断掉就得从头再来。更工程化的是前端分片上传:把文件切成固定大小的块(比如 5MB),逐块上传,服务端按文件唯一标识(通常是文件 MD5)把块拼起来。这样有三大好处:单请求小、不触发各种大小限制;断网后只重传失败的块;还能并发上传提速。
服务端拼接前必须做校验,否则会产生损坏文件。最稳妥的顺序是:所有分片到齐后,先按 MD5 拼出完整文件,再和客户端上报的整体 MD5 比对,一致才算成功。合并命令很简单:
# 分片按序号命名 part_000, part_001 ...
cat part_* > upload_full.zip
md5sum upload_full.zip这里有个坑:分片的顺序必须严格按数字序号,而不能按文件系统的字母序——part_10 排在 part_2 前面会把文件拼坏。正确做法是在服务端按索引排序后再拼,而不是靠 shell 通配符的自然展开。
别忘了 CDN 和代理的超时
如果你的站套了 CDN,还要检查两件事。一是 CDN 对单请求体积的上限(很多免费套餐是 100M),超过就只能走分片;二是 CDN 的回源超时,大文件慢速上传很容易触发 524 之类的超时错误。这类问题的通用解法还是分片——把大请求拆小,让每个请求都能在超时窗口内完成。定位时可以在源站 Nginx 日志里看 $request_time 和 $upstream_response_time,判断是卡在源站还是边缘。
进度条与超时重试:体验层也要跟上
放开限制只是让上传「能成功」,但大文件上传最劝退用户的其实是「卡着不动,不知道还要多久」。前端用 XMLHttpRequest 的 upload.onprogress 事件就能拿到实时进度,配合分片还能显示「第几块/共几块」:
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const pct = (e.loaded / e.total * 100).toFixed(1);
console.log('已上传 ' + pct + '%');
}
};
xhr.open('POST', '/upload_chunk');
xhr.send(chunkBlob);xhr.upload 上的进度事件只反映「数据发出去」的进度,不代表服务端已经写完磁盘。真正可靠的完成信号要看响应——服务端在分片文件落盘并做完整性校验后返回成功,前端才更新「这一片完成」。对慢速网络,单个分片的超时时间也要设够,建议片上超时 60 秒、整文件总超时另设,避免用户传了几十兆因为一片超时而全部重来。
最后是重试策略。断点续传的核心是「上传前先问服务端哪些分片已经收过了」。流程是:开始上传时先带文件 MD5 询问进度,服务端返回已存在的分片序号数组,前端只传缺失的部分。这样无论中途断多少次,重新进入页面都能从断点继续,而不是从头再来。分片大小也有讲究:太小会让请求数暴增、合并开销大,太大会让单片失去抗断的意义,实践中 5M 到 10M 是比较舒服的区间。
临时目录与磁盘:别让分片填满 / 分区
分片上传会把所有临时分片先落到磁盘上再合并,这块空间往往被忽略。如果临时目录和系统盘是同一块,几 G 的分片很可能会把根分区写满,直接把站点拖挂。稳妥做法是把上传临时目录指到独立的、容量充足的数据盘,并定期清理超过一定时间的孤儿分片——那些上传到一半就再没回来的残留:
# 清理 1 天前的临时分片
find /data/upload_tmp -type f -mtime +1 -delete把这个清理任务放进 cron 每天跑一次,比等到磁盘告警再手忙脚乱强得多。合并完成后也要及时删掉对应的分片目录,否则每个成功上传都会留下一份冗余副本,日积月累就是空间黑洞。
另外值得单独提一句的是「上传目录的可执行权限」。很多站点把所有上传文件放在网站根目录下,一旦有人上传了伪装成图片的脚本文件,就可能被直接访问执行,这是最经典的上传漏洞。稳妥的做法是上传目录禁止脚本执行,在 Nginx 里关掉该目录的 PHP 解析,并强制附件以下载方式返回而不是内联渲染:
location ~* ^/uploads/.*\.(php|phtml|php5)$ {
deny all;
}
location /uploads/ {
add_header Content-Disposition "attachment";
add_header X-Content-Type-Options nosniff;
}nosniff 头能阻止浏览器把内容「猜」成可执行类型,配合禁止解析,双保险堵住上传型攻击。放开上传大小是为了让用户方便,但方便不能以牺牲安全为代价——两者完全可以兼得。
收尾清单
- 先用 curl -I 看清 413 是 Nginx、CDN 还是应用层返回的。
- Nginx 放开 client_max_body_size,代理层加 proxy_request_buffering off。
- PHP 的 upload_max_filesize / post_max_size / max_execution_time 三处一起调,改完 restart php-fpm。
- 用 limit_rate + limit_rate_after 在放开大小的同时管住带宽。
- 真正的大文件走分片 + 断点续传,拼接时按数字序号排序并校验整体 MD5。
- 套 CDN 的站点同时核对边缘体积上限与回源超时。
413 从来不是一个参数的问题,而是一条链路的问题。理解了请求从浏览器到磁盘经过了哪些层、每层在哪拦、怎么放,你就能既让用户顺利传上大文件,又不被大文件拖垮整站。