为什么「文件明明上传了」还是 413
个人站长用 Nginx 做反向代理时,最容易被 413 Request Entity Too Large 这条报错绕进去:明明已经在 server 块里写了 client_max_body_size 100m;,用 curl 传一个 60MB 的包还是被拒。于是开始怀疑 Nginx 没重载、怀疑 CDN 拦截、怀疑 PHP 限制——最后往往在一个和上传完全无关的层上找到答案。
这篇文章不讲「把参数改大试试」,而是把 client_max_body_size 这个值到底在多少个地方被检查讲清楚。读完你应该能判断:当 413 出现时,到底是哪一层先拒绝的,以及为什么改一处常常不够。
第一层:client_max_body_size 到底管的是什么
先纠正一个常见误解。client_max_body_size 限制的不是「文件大小」,而是「请求体的 Content-Length」。这两者在普通表单上传时数值接近,但在下面这些场景里会严重不一致:
# 请求体 = 文件本身 + multipart 边界 + 其他表单字段 + base64 膨胀
# 一个 40MB 的图片,用 base64 编码塞进 JSON 后,请求体可能接近 54MB
# 一个「一次选 20 个文件」的前端,请求体是 20 个文件之和,不是单文件大小所以当用户抱怨「我就传了一张 50MB 的图」,实际请求体可能是 70MB。如果你的 Nginx 限制写的是 50m,那就正好卡在中间:单文件没超,请求体超了。这也是为什么把限制从 50m 调到 100m 时常「突然就好了」——不是玄学,是你终于覆盖了真实请求体。
第二层:http、server、location 三处的生效规则与覆盖陷阱
这是最容易出错的一环。Nginx 的指令继承不是「就近相加」,而是就近覆盖:只要某个 location 里写了 client_max_body_size,那么它下面的所有请求都只看这个值,不会再往上取 server 层的值。
http {
client_max_body_size 20m; # 全局默认
server {
server_name upload.example.com;
client_max_body_size 100m; # 本站 100M
location /upload/ {
# 这里没写 -> 继承 server 的 100m,正常
proxy_pass http://backend;
}
location /legacy/ {
client_max_body_size 2m; # 只留给老接口,写完就覆盖了上面的 100m
proxy_pass http://backend;
}
}
}
# 常见事故:从别处拷来的 location 段里夹带了 client_max_body_size 1m;
# 结果 /upload/ 下的某个子路径突然只能传 1MB,而 server 层的 100m 完全不起作用排查动作很具体:把配置里所有出现该指令的地方一次性列出来,这是唯一能快速定位「谁覆盖了谁」的办法。
nginx -T 2>/dev/null | grep -n "client_max_body_size"nginx -T(注意大写 T)会把 include 进来的所有文件展开后输出,比逐个文件去翻可靠得多。看到输出里某个 location 出现在你不记得写过的地方,基本就是答案。
第三层:反向代理后,后端自己也在拒绝
即使 Nginx 这一层放行了,请求还可能死在 Nginx 之后。这条链路每加一跳,就多一个「体积判断」:
浏览器
→ CDN / WAF ← 常有自己的 body 上限,且多数不对用户暴露
→ Nginx (client_max_body_size)
→ PHP-FPM ← post_max_size / upload_max_filesize
→ 应用框架 ← 自己的 maxContentLength / bodyParser limitPHP-FPM 这一层的判定有个非常反直觉的行为值得单独记一下:
upload_max_filesize只约束单个文件;post_max_size约束整个请求体。两者都要调,只调一个会得到「有时候行有时候不行」的结果。- 当请求体超过
post_max_size时,PHP 会把$_POST和$_FILES清空成空数组,而不是报一个明确的错。前端看到的现象通常是「提交成功但什么都没收到」,非常难查。 - 改完 php.ini 后必须重启 php-fpm(
systemctl restart php-fpm),reload 不会重读 ini。
php -i | grep -E "post_max_size|upload_max_filesize|memory_limit"
# 注意:CLI 的 php.ini 和 FPM 的可能不是同一个文件
# FPM 要看:php-fpm -i 或者直接看 /etc/php/*/fpm/php.ini第四层:代理层怎么把 413「翻译」掉了
很多人查不到 413,是因为它压根没到浏览器。Nginx 向上游发请求时,如果上游返回 413,而 Nginx 自己配置了错误页接管,用户看到的可能是一个面目全非的页面:
location /upload/ {
client_max_body_size 100m;
proxy_pass http://backend;
proxy_intercept_errors on;
error_page 413 = /413.html; # 413 被换成了自定义页
}
还有一种更隐蔽的情况:前端用的是 XMLHttpRequest 或 fetch,而 413 响应体是空的,JS 又只处理 response.data,于是控制台里只有一句「上传失败」,看不到状态码。排查时先看 Network 面板的 Status 列,不要先看 Console。
如果你的上传走 CDN(比如站点套了 CDN 回源),还要意识到 CDN 的 body 限制通常低于源站。修好源站后仍然 413,去 CDN 控制台找「最大上传大小」这类设置,它不在 Nginx 里。
第五层:不只是 413——413 修好后暴露出的 502 / 504
这是很有价值的一类经验:把体积限制放开后,故障形态往往从 413 变成 502 或 504。原因很朴素:
- 原本大请求在入口就被拒,后端根本收不到,压力是「零」;
- 放开之后,后端真正开始处理几十 MB 的请求,如果它需要把整个 body 读进内存(很多框架默认行为),内存尖峰直接触发 OOM Killer → 502;
- 如果后端处理慢,超过
proxy_read_timeout默认的 60 秒 → 504。
所以放开限制时,要同步把超时一起调,并且给后端的 body 读取设一个磁盘缓冲:
location /upload/ {
client_max_body_size 100m;
client_body_buffer_size 1m; # 超过 1m 的部分落到 client_body_temp_path 的磁盘
client_body_temp_path /var/cache/nginx/client_temp;
proxy_pass http://backend;
proxy_connect_timeout 10s;
proxy_send_timeout 300s; # 大文件上传,发送方向也要给够
proxy_read_timeout 300s; # 后端处理慢时别 60s 就断
}proxy_send_timeout 经常被忽略。它是 Nginx 向后端发送请求体的超时——上传大文件时,如果后端读得慢,Nginx 这边发送就会被拖住,超时后连接中断,用户看到的是「传了 90% 然后失败」。这类 90% 失败几乎都指向发送/接收超时,而不是体积限制。
一份可以直接用的排查顺序
下次遇到 413,按下面顺序走,通常五分钟内能定位:
# 1. 先看是谁返回的 413(Nginx 还是后端)
curl -i -X POST --data-binary @big.bin https://site/upload/ | head -20
# 看 Server 头:nginx 还是后端框架自己的名字
# 2. 列出配置里所有体积限制的落点
nginx -T 2>/dev/null | grep -n "client_max_body_size"
# 3. 确认后端自己的限制
php -i | grep -E "post_max_size|upload_max_filesize"
# 4. 构造一个可控体积的测试体,二分法找阈值
for sz in 1 5 10 20 50; do
head -c ${sz}M /dev/urandom > /tmp/t.bin
echo -n "${sz}M: "
curl -s -o /dev/null -w '%{http_code}\n' -X POST --data-binary @/tmp/t.bin https://site/upload/
done
# 5. 看错误日志里真实的拒绝原因(别只看 access.log 的状态码)
tail -50 /var/log/nginx/error.log第 4 步的二分法非常实用:它把「到底哪一层拒绝」变成一个可以量化的阈值。当你发现阈值恰好是 post_max_size 的值而不是 client_max_body_size 的值时,答案就直接写在那里了,不用再猜。
顺带一个常被忽略的点:client_body_temp_path 写在哪
放开体积限制后,超过 client_body_buffer_size 的部分会被 Nginx 落盘暂存。这个临时目录的位置经常是「容器里没挂载」的那一个:
# 默认通常在 /var/lib/nginx/body 或编译时指定的路径
nginx -V 2>&1 | tr ' ' '\n' | grep client-body-temp-path
# 显式指定并把权限给对(Nginx worker 用户需要可写)
client_body_temp_path /var/cache/nginx/client_temp 1 2;
# 1 2 表示两级子目录哈希,文件多时避免单目录 inode 膨胀如果这个目录不可写,Nginx 会在落盘时失败,报错仍然是 500 或 413 一类的形态,而不是「目录权限错误」这么直白。容器化部署时尤其常见:镜像里 /var/cache/nginx 没有持久化卷,重建容器后目录消失。自查方式是 grep client_body_temp_path /var/log/nginx/error.log,命中一次就说明曾经触发过。
多文件上传:为什么「单个都没超」还是被拒
前端一次选多个文件时,浏览器对 multipart/form-data 的处理方式决定了一切。主流两种情况:
- 全部塞进一个请求:请求体是所有文件之和,你必须有
client_max_body_size >= 总大小。用户传 5 个 30MB 的文件,请求体是 150MB 以上,而你的限制是 100MB。 - 每个文件一个请求:单个请求不超,但会并发。这时真正的瓶颈不是体积,而是
worker_connections与worker_processes够不够,以及后端能否扛住并发写入。
判断你的站属于哪种,最快的方法是在浏览器 Network 面板看上传期间有几条 /upload/ 记录。一条 = 合并请求,多条 = 分片/并发。两种情况的调优点完全不同:前者调体积,后者调并发与限速,别混着调。
如果确实是合并请求又不想放开到 500MB,正确的做法是在前端改成逐文件上传(或分片),而不是把 client_max_body_size 一路加到没有上限——后者会让超大请求成为 DoS 的入口,属于用可用性换安全性的坏交易。
小结:413 是一个「分层拒绝」信号,不是一个参数问题
把结论收一下:
client_max_body_size限制的是请求体而非文件,多文件与 base64 会让两者严重偏离;- 三处配置是就近覆盖而非相加,必须用
nginx -T | grep全量确认落点; - Nginx 放行不等于后端放行,PHP 的
post_max_size超限时会静默清空$_FILES; - 放开体积后要预期故障形态迁移到 502/504,同步调
proxy_send_timeout与proxy_read_timeout; - 日志里的状态码只是结果,
error.log里的原因才是指向。
对个人站长来说,这类问题的价值不在「修好一次」,而在于建立一张清单:以后任何「请求被拒」类故障,都先问一句——这一跳之后,还有几跳?每一跳的上限是多少?把这张清单填完,故障基本就自证了。