Nginx 413 文件上传失败排查与根治

你有没有碰到过这样的场景:前端上传一个几十 MB 的文件,浏览器直接弹出「413 Request Entity Too Large」,但后端接口明明没做大小限制?Nginx 413 是文件上传失败里最高频的拦路虎,根因几乎都出在 client_max_body_size 与层层代理的限制上。本文带你从现象到根因,把这条上传链路彻底讲透。

一、413 到底是什么:谁在拒绝你

HTTP 413 状态码的全称是 Request Entity Too Large,意思是「客户端发来的请求体太大,服务器拒绝接收」。关键点在于:这个错误通常由反向代理或网关返回,而不是你的应用代码。换句话说,请求根本还没进到 Spring Boot、Node 或 PHP,就在 Nginx 这一层被拦下了。这也是它和 502、504 最本质的区别——后两者是「上游出问题」,而 413 是「你太大了,我压根不收」。

状态码含义责任方典型诱因
413请求体过大被拒Nginx / 网关client_max_body_size 过小
502网关拿到坏响应上游应用后端崩溃、端口错配
504网关等待超时上游应用慢 SQL、死循环、阻塞

如果你看到的不是 413 而是 502/504,问题方向完全不同,可参考《502/504 线上故障排查》对症下药。

二、第一嫌疑人:client_max_body_size

Nginx 默认 client_max_body_size1m(1 兆字节)。只要请求体超过这个值,Nginx 会直接返回 413,连后端影子都见不到。它的作用域是 http / server / location 三层,内层会覆盖外层。

# 放在 http / server / location 任意一层,作用域逐级覆盖
server {
    listen 443 ssl;
    server_name upload.example.com;

    # 全局放宽到 50M
    client_max_body_size 50m;

    location /api/upload {
        # 上传接口单独放宽到 200M
        client_max_body_size 200m;
        proxy_pass http://app:8080;
    }
}

注意作用域继承:location 没写就继承 server,server 没写继承 http。很多人只在 http 层改了,结果被某个 server 块的默认值覆盖,排查半天以为没生效。所以定位时一定用 nginx -T 打印出完整生效配置来确认,而不是只盯着自己改的那一行。

三、代理链:你以为改了一处,其实层层设卡

真实生产环境不是「客户端 → Nginx → 应用」这么简单,而是一条代理链。每一层都可能限制请求体大小,任何一层没放开都会 413。

链路层级限制项配置位置
浏览器 / 前端无硬限制,靠 JS 校验FormData / axios
CDN / 云 WAF单请求体上限(如阿里云 WAF 默认 32M)厂商控制台
负载均衡 SLB / ALB请求体上限厂商控制台
Nginx 反向代理client_max_body_sizenginx.conf
应用前置 uwsgi / fastcgi各自的 body 限制uwsgi.ini 等
后端框架multipart 上限Spring / Tomcat 配置
# nginx 反代到 uwsgi 时,uwsgi 也有自己的限制
location / {
    include uwsgi_params;
    uwsgi_pass 127.0.0.1:3031;
    # uwsgi 模式下同样受 client_max_body_size 约束
    client_max_body_size 200m;
}

这就是为什么改了 Nginx 还报错——uwsgi 或后端框架(如 Spring 的 multipart 上限、Tomcat 的 maxPostSize)还在悄悄拦。关于 Nginx 反代本身的完整配置姿势,可看《Nginx 反向代理完整配置:负载均衡 + HTTPS + 缓存优化》

四、三步定位:到底是谁在拦

不要靠猜,用证据说话。下面这套流程能快速锁定责任层。

步骤 1:用 curl 原样复现(绕过浏览器缓存和前端 JS 校验):

# 准备一个 100M 的测试文件
dd if=/dev/zero of=big.bin bs=1M count=100

# 直接打 Nginx,看返回码
curl -s -o /dev/null -w "HTTP %{http_code}" -F "file=@big.bin" https://upload.example.com/api/upload

# 看响应头里有没有 Nginx 标记
curl -s -D - -o /dev/null -F "file=@big.bin" https://upload.example.com/api/upload | grep -i server

步骤 2:查 Nginx 错误日志,关键词非常直白:

# 报错长这样
# [error] 1234#0: *56 client intended to send too large body: 104857600 bytes

grep "too large body" /var/log/nginx/error.log

这条日志一出现,就坐实是 Nginx 的 client_max_body_size 拦的。如果日志里没有、但 CDN 返回 413,那多半是 CDN / WAF 层在拦。

步骤 3:逐层缩小。把 curl 直接打到「应用端口」跳过 Nginx,再打到「SLB 之后的 Nginx」跳过 CDN。哪一层开始返回 413,就是哪一层的问题,定位精确到分钟级。

五、不同场景怎么设才合理

不要图省事直接 client_max_body_size 0(不限制),那是给自己埋雷。按需、分层设置才是正解。

场景建议做法
普通表单 / 头像默认 1m~8m 足够,不用改
图片 / 文档上传单接口放宽到 20m~50m
视频 / 大附件单接口 200m+,并配合前端分片
全站兜底保持 http 层默认,只在 location 放开
// 前端分片上传,避免单次超大请求把任何一层打爆
async function uploadInChunks(file, chunkSize = 5 * 1024 * 1024) {
  for (let start = 0; start < file.size; start += chunkSize) {
    const blob = file.slice(start, start + chunkSize);
    await fetch('/api/upload?offset=' + start, {
      method: 'POST',
      body: blob,
    });
  }
}

分片不仅绕开了单请求体上限,还能断点续传、并发加速,是视频类大文件上传的标准解法。

六、最容易踩的 5 个坑

1. 改了配置没 reload。 nginx -s reload 才生效,改完不 reload 等于没改,这是最高频的「我明明改了为啥还 413」。

2. 只改 server 漏了 location。 被更内层的默认值覆盖,用 nginx -T 看真实生效值。

3. 忘了 CDN / SLB 也有上限。 Nginx 放开了,云 WAF 或 SLB 还在拦,控制台也得同步调。

4. 设成 0 不设上限。 攻击者可以传超大文件把磁盘打满(排查思路可参考《磁盘 inode 耗尽排查》),务必加应用层校验和硬上限。

5. 后端框架上限忘了调。 Spring Boot 默认 multipart 1MB,Tomcat 的 maxPostSize 也要同步看,否则 Nginx 放开了后端照样拒。

七、一页排障清单

[ ] curl 原样复现,确认返回 413
[ ] 看响应 Server 头,判断是否 Nginx 返回
[ ] grep Nginx error.log 找 "too large body"
[ ] 检查 http / server / location 三层的 client_max_body_size
[ ] 检查 uwsgi / 后端框架上限
[ ] 检查 CDN / WAF / SLB 控制台的请求体上限
[ ] nginx -s reload 后重试

八、进阶:client_body_buffer_size 与 413 不是一回事

很多同学会把 client_max_body_sizeclient_body_buffer_size 搞混,其实二者职责完全不同。前者是「硬上限」,超过直接 413 拒绝;后者是「缓冲区大小」,决定请求体是先放内存还是先落磁盘(临时文件)。当请求体超过 client_body_buffer_size 时,Nginx 会把它写入 client_body_temp_path 指定的临时目录,而不是立刻报错。

这里有个隐藏雷区:如果那个临时目录权限不对、或者磁盘 inode 被占满(可参考《磁盘 inode 耗尽排查》),写入临时文件会失败,同样可能表现为上传异常。另外在反代场景下,proxy_request_buffering off 会关闭请求体缓冲、边收边转发,此时对 client_max_body_size 的限制依然有效,但绕开了本地落盘,适合大文件流式上传。

九、实战复盘:一次线上头像上传 413 的完整定位

讲一个真实案例。某天客服反馈:「用户换头像偶尔报上传失败,刷新重试又好了」。听起来像偶发,但我们按本文流程一查就定位了根因。

# 第一步:grep 错误日志,确认是 Nginx 在拦
grep "too large body" /var/log/nginx/error.log
# 发现有零星记录,但 body 大小只有几 MB,远没到 50m

# 第二步:nginx -T 看真实生效配置
nginx -T | grep -A2 "location /api/avatar"
# 发现该 location 没写 client_max_body_size,继承到了 http 层的 1m 默认值

真相大白:头像接口一开始用的是 1m 默认值,后来有人在 http 层统一改成了 50m,却忘了这个 location 因为历史上单独配过、并没有继承到新值。偶发的原因是——部分流量走的是另一个没单独配的 location,所以「重试就好」。

修复只需在对应 location 显式加上 client_max_body_size 20m;nginx -s reload 后彻底消失。这个案例说明:用 nginx -T 看真实生效值,比相信自己改的那一行可靠得多

Nginx 413 看似简单,背后却是一条需要逐层排查的代理链。记住一句话:先 curl 复现、再 grep 日志、最后逐层放开,别急着把 client_max_body_size 设成 0。把上传失败这类高频问题沉淀成清单,下次值班同学照着勾就行。

上一篇 Human-in-the-loop 实战:让 AI Agent 在关键节点等人确认
下一篇 Semantic Router 语义路由实战:精准分发用户请求