news 2026/6/9 20:04:22

踩坑:Gateway 请求体只能被消费一次?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
踩坑:Gateway 请求体只能被消费一次?
    • 为什么请求体只能读一次?
    • 那怎么解决?—— 把 body “缓存”起来
    • 注意事项 & 我们的踩坑点
    • 有没有更简单的办法?
    • 我的看法

这个问题我是在写一个日志记录功能时撞上的。当时想在 Spring Cloud Gateway 里加个全局过滤器,把所有进来的请求参数(尤其是 POST 的 JSON)打个日志,方便排查问题。结果发现——请求体读了一次之后,下游服务就收不到 body 了!

一开始我还以为是代码写错了,反复检查,后来才明白:Reactor + Netty 环境下,请求体(RequestBody)默认只能读一次。这不是 bug,是设计如此。

今天就聊聊这个“坑”,以及我们是怎么绕过去的。


为什么请求体只能读一次?

在传统的 Servlet 里,HttpServletRequest的输入流可以被多次读取(虽然也不推荐),因为 Tomcat 底层做了缓冲。但 Gateway 不一样。

Gateway 基于 WebFlux,底层用的是 Netty。Netty 为了高性能,不会把整个请求体缓存在内存里。它是一个字节流,像水管一样,数据流过去就没了。你用ServerHttpRequestgetBody()拿到的是一个Flux<DataBuffer>,本质上是个只能消费一次的流

一旦你在 Filter 里把它subscribecollect读完了,下游路由到微服务的时候,body 就空了。

我们的经验是:任何试图直接读取原始 body 的操作,都会导致后续服务拿不到数据

比如下面这段“看似正常”的代码:

@ComponentpublicclassLogGlobalFilterimplementsGlobalFilter{@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){ServerHttpRequestrequest=exchange.getRequest();// 危险!这样读完,body 就没了returnDataBufferUtils.join(request.getBody()).flatMap(dataBuffer->{Stringbody=StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer()).toString();System.out.println("请求体: "+body);// 这时候 body 已经被消费掉了!returnchain.filter(exchange);});}}

跑起来你会发现:日志是打出来了,但下游服务收到的 POST 请求是空的,直接报错“缺少参数”。


那怎么解决?—— 把 body “缓存”起来

关键思路是:读一次 body,然后重新构造一个新的 ServerHttpRequest,把读到的内容“塞回去”

Spring 提供了ServerHttpRequestDecorator,可以让我们包装原始请求。

下面是我们最终能用的版本:

@ComponentpublicclassCacheBodyGlobalFilterimplementsGlobalFilter,Ordered{@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){// 只处理有 body 的请求,比如 POST/PUTif(exchange.getRequest().getHeaders().getContentLength()>0){returnDataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer->{// 保留一份 byte 数组byte[]bytes=newbyte[data_BUFFER.readableByteCount()];dataBuffer.read(bytes);DataBufferUtils.release(dataBuffer);// 释放原始 buffer// 构造新的请求体NettyDataBufferFactorynettyDataBufferFactory=newNettyDataBufferFactory(ByteBufAllocator.DEFAULT);DataBufferbodyDataBuffer=nettyDataBufferFactory.allocateBuffer(bytes.length);bodyDataBuffer.write(bytes);// 重写 requestServerHttpRequestnewRequest=newServerHttpRequestDecorator(exchange.getRequest()){@OverridepublicFlux<DataBuffer>getBody(){returnFlux.just(bodyDataBuffer);}};// 把新请求放回 exchangeServerWebExchangenewExchange=exchange.mutate().request(newRequest).build();// 打印日志(或者做其他事)StringbodyStr=newString(bytes,StandardCharsets.UTF_8);System.out.println("缓存后的请求体: "+bodyStr);returnchain.filter(newExchange);});}returnchain.filter(exchange);}@OverridepublicintgetOrder(){return-100;// 尽量靠前,确保在其他逻辑前执行}}

这段代码的核心就是:

  1. DataBufferUtils.join()把流聚合成一个DataBuffer
  2. 转成byte[]保存下来。
  3. ServerHttpRequestDecorator重写getBody()方法,返回我们缓存的数据。
  4. exchange.mutate().request(...).build()替换掉原来的请求。

这样,后续的过滤器和下游服务拿到的还是完整的 body。


注意事项 & 我们的踩坑点

  • 别忘了 release 原始 DataBuffer!Netty 的内存管理很严格,不释放会导致内存泄漏。DataBufferUtils.release(dataBuffer)很关键。
  • 只对需要读 body 的请求做缓存。GET 请求没 body,没必要处理,还能省性能。
  • 大文件上传别这么干!如果有人 POST 一个 100MB 的文件,你全读进内存,服务直接 OOM。所以最好加个 body 大小限制,比如只缓存小于 1MB 的请求。
  • 编码问题:我们固定用了UTF-8,如果你的系统用别的编码,记得改。

有没有更简单的办法?

其实 Spring Cloud Gateway 官方也意识到这个问题了。如果你只是想打印日志,可以用现成的ModifyRequestBodyGatewayFilterFactory,它内部已经做了 body 缓存。

但如果你想在 Filter 里自己处理 body 内容(比如验签、解密、改字段),那就得手写上面那种逻辑。


我的看法

我认为,这个“只能读一次”的设计虽然反直觉,但其实是合理的。高性能网关不应该默认把整个请求体缓存起来,那样太浪费内存。要不要缓存,应该由业务决定

只是作为开发者,得清楚这个前提:在响应式流里,数据流是一次性的,想重复用,就得自己存一份

现在每次写 Gateway 的 Filter,我都会先问一句:“这里要读 body 吗?” 如果要,立马套上缓存模板,不敢偷懒。

希望这篇碎碎念能帮你少走点弯路。毕竟,谁也不想 debug 一整天,最后发现是 body 被吃掉了

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/8 15:02:27

Ring-mini-linear-2.0:1.6B激活参数实现8B性能的高效大模型

Ring-mini-linear-2.0&#xff1a;1.6B激活参数实现8B性能的高效大模型 【免费下载链接】Ring-mini-linear-2.0 项目地址: https://ai.gitcode.com/hf_mirrors/inclusionAI/Ring-mini-linear-2.0 导语 inclusionAI团队近日开源的Ring-mini-linear-2.0模型引发行业关注…

作者头像 李华
网站建设 2026/6/8 19:33:17

基于python框架的电影订票系统_wqc3k--论文_pycharm django vue flask

目录已开发项目效果实现截图开发技术路线相关技术介绍核心代码参考示例结论源码lw获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;已开发项目效果实现截图 同行可拿货,招校园代理 基于python框架的电影订票系统_wqc3k–论文_pycharm django v…

作者头像 李华
网站建设 2026/6/8 19:25:34

【工业级RPA新标准】:Open-AutoGLM群控如何实现毫秒级指令同步?

第一章&#xff1a;工业级RPA的演进与Open-AutoGLM群控的诞生随着企业自动化需求的不断升级&#xff0c;传统RPA&#xff08;机器人流程自动化&#xff09;在面对复杂、动态和高并发的工业场景时逐渐暴露出扩展性差、维护成本高和智能化程度低等瓶颈。工业级RPA应运而生&#x…

作者头像 李华
网站建设 2026/6/8 19:31:09

降ai率从85%到15%!暴力实测10款降ai神器,这款降ai工具真的神了!

我敢说降AI率有手就行&#xff0c;这不是易如反掌&#xff1f;本人就是这么自信&#xff0c;想当年我的论文降ai可是一次过&#xff0c;稳得连导师都挑不出毛病。 很多人对着红通通的查重报告发愁&#xff0c;想知道我是怎么做的吗&#xff1f;真相只有一个----当然是借助科技…

作者头像 李华
网站建设 2026/6/8 18:57:49

python智能停车计费系统设计与实现_urqs9--论文_pycharm django vue flask

目录 已开发项目效果实现截图开发技术路线相关技术介绍核心代码参考示例结论源码lw获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01; 已开发项目效果实现截图 同行可拿货,招校园代理 python智能停车计费系统设计与实现_urqs9–论文_pycharm dj…

作者头像 李华