生产复盘:请求报413 Request Entity Too Large问题分享

有天下午生产群里甩进来一张截图:生产环境 http://10.3.87.23:8080/api/auth/getuserlist 接口返回异常:413 Request Entity Too Large。
做后端的人看到 413 的第一反应都是"请求体太大"。可我把报文要过来一看,body 就六十几字节:
{"appid":"b8162beeae174d6d862777a3a25344xx","warehouseid":"3899ba7ae020e81180c5a0369f7789zz","staffid":"20003712119314432"}
这点东西能撑爆谁?当时就有点懵。
更邪门的是现象本身:个别账号必现,其他账号从来没这个问题。而且同一个账号,浏览器里点必挂,把请求原样复制成 curl 到命令行跑,又好了。按账号出问题的 bug 最讨厌——第一嫌疑就是权限、数据范围、灰度这种跟用户维度挂钩的逻辑,查起来一头雾水。
先复现,再缩小包围圈
排查的第一步永远是复现。把浏览器里的请求复制出来逐段删,终于找到了那个开关:Cookie。curl 不加 -b 一切正常,把 Cookie 那段加上,413 马上回来。
curl 'http://10.3.87.23:8080/api/auth/getuserlist' \
-H 'Authorization: Bearer 5c400750-176f-4963-9fe7-...' \
-b $'homeSetting={"section1":{"component":"out",...},"middleStatus":true}; access_token=5c400750-...; user=2000371211931443201' \
...
Cookie 里躺着三样东西:一段前端存的页面布局配置 homeSetting、一个跟 Authorization 重复的 access_token、一个 user id。加一起也就两三百字节。两三百字节把一个生产接口搞挂,说出去谁信?但它就是这么发生了。
当前请求的链路是 nginx → gateway → auth 三层,那就一层层排除。
先怀疑 nginx。413 在 nginx 语境下几乎等于 client_max_body_size,可我们翻配置,上限 10M,而且加不加 Cookie,body 一个字都没变,nginx 没理由拦。排除。
再看 auth 服务。把出问题的时间窗内的访问日志拉出来翻——干净得不像话,这条请求根本没到业务层。网关自己的 access log 里同样找不到它,说明连网关的过滤器都没跑进去。
到这里结论其实已经浮出来了:请求死在网关的 Netty 解码层,还没进 Spring 就被按 413 扔了。
7K 的头,加上 300 字节的 Cookie
现在回到那个"个别账号"。把好账号和坏账号的完整报文摆在一起对比,差异一目了然——坏账号的请求头里躺着一串巨长的业务头:
Ownerid: 79a02f33f97b424c82214e6d26f1db2c,995688131075180,8d0e2beee09a4184a3a696b0d2e27ba1,...
Ownername: %E6%88%90%E9%83%BD%E9%9A%86%E7%A7%91%E5%BA%B7%E6%BA%90%E5%8C%BB%E8%8D%AF%E6%9C%89%E9%99%90%E5%85%AC%E5%8F%B8%2C...
Ownerno: CTUA-WTF86732,CTU-WTF019,CTUA-WTF77949,...
当时统计了一下:Ownerid 一个头里挤了 51 个 ID;Ownername 是跟它一一对应的五十多家公司全名,而且是 URL 编码过的中文,一个汉字占九个字节;Ownerno 再来一份。光这三个头加起来就 7K 上下。
这些头不是前端乱加的。平台的登录态里存着"这个账号在哪些货主、哪些仓库、哪些租户底下有权限",请求带着这串身份上下文走完全程:网关做灰度要拿 Ownerid 去 Redis 里比对,下游接口要拿它过滤数据。账号授权面越宽,这串头就越长。那个必现的账号,是同时在五十多家货主单位底下有授权的"大账号";而普通账号名下就一两家,整包请求头 1K 出头,永远碰不到天花板。
把报文存成文件量了量字节数:不带 Cookie 大约 7.9K,浏览器带上 Cookie 直接 8.1K。
8K 是什么概念?Netty 的 HttpObjectDecoder 默认 maxHeaderSize = 8192,reactor-netty 的 HttpDecoderSpec 也是同样的 8K 默认值。请求头累计超过这个数,解码阶段直接拒绝,连业务代码的影子都见不到。8K 对正常请求来说其实很宽裕,但"五十多个货主的授权清单 + 一个叠了 homeSetting 的 Cookie",就刚好把它顶破。
所以所谓"个别账号必现"根本不是账号逻辑问题,是授权特别多的大账号必现,我们一开始把相关性误当成了因果。Cookie 也不是元凶,它只是压垮骆驼的那根稻草——没有它,7.9K 还在线内;有了它,8.1K,必挂。
那怎么解决
定位到根因之后,修复本身反而最省事:把网关 Netty 的解码上限调大。
但坑在这个项目的版本上——Spring Boot 2.1.6 + Spring Cloud Greenwich.SR2,reactor-netty 0.8.x。这个年代的 WebFlux 对 Netty 几乎不给配置口子:社区后来加的那些 max-header-size 配置项是后面版本才有的,这个版本不认;网上搜出来的解决方案十有八九是 servlet 容器(Tomcat/Undertow)的心得,抄过来根本不生效。折腾一圈,唯一有效的方式就是 WebServerFactoryCustomizer 编程式定制:
@Configuration
public class NettyServerConfiguration {
@Value("${server-max-http-header-size:65536}")
private int maxHeaderSize;
@Bean
public WebServerFactoryCustomizer nettyServerCustomizer() {
return factory -> factory.addServerCustomizers(httpServer ->
httpServer.httpRequestDecoder(spec ->
spec.maxHeaderSize(maxHeaderSize)
.maxInitialLineLength(maxHeaderSize)));
}
}
默认放宽到 64KB(留着配置项可覆盖),顺手把 maxInitialLineLength 也提到同样高度——请求行默认 4KB 的上限是同一类雷,哪天 URL 里塞个长参数照样炸。
发版之后,网关日志里打出 [NettyConfig] 正在应用 maxHeaderSize=65536,再拿那份带 Cookie 的报文跑,200,收工。
第二步是前端请求接口时跟业务确认,头信息中只用传Ownerid,Ownername等都不用传,这样就减少80%的量。
回头复盘
这个网关的 8K 默认值只是背锅的,真正的隐患是两头:
一是把业务上下文全塞进 Header 这个设计。Ownerid 和 Ownername、Ownerno 三份数据一一对应,纯冗余——公司全名这种事后端一条 SQL 就能查回来,却要以 URL 编码中文的形式跟着每个请求跑一路。授权面大的账号,头部体积跟着授权数量线性膨胀,迟早出事。该压缩的压缩,该去重的去重,实在不行挪到登录态里由后端恢复,别让带宽和 Netty 一起买单。
二是前端把 homeSetting 这种页面配置也往 Cookie 里塞的习惯。Cookie 是每次请求自动携带的,几百字节看着不起眼,但在这种头部本来就贴着上限的请求上,它就是压死骆驼的那根草。放 localStorage 不香么?
还有一点体会:这种解码层的拒绝发生在任何业务代码之前,业务日志完全无感,网关自己的 access log 都留不下痕迹。要不是用户截图,我们根本不知道有这回事。类似的拦截最好单独留错误日志、挂上告警,不然下次还得靠客户当我们的监控。
最后把两个数字刻进脑子里:Netty 请求行默认上限 4096,请求头默认上限 8192。老版本的默认值不会写在报错里,但它一直在那。
原文地址: https://www.cveoy.top/t/topic/qHvN 著作权归作者所有。请勿转载和采集!