Skip to content

Support cleanly discarding body in WebTestClient #35953

@krezovic

Description

@krezovic

At the moment, I find no equivalent of WebClient's toBodilessEntity within WebTestClient.

Use-case: Simply inspect that response is returned when its content cannot be inspected or it makes no sense for it to be inspected.

I have the following workaround which requires a lot of boilerplate

var res = client.mutate()
        .responseTimeout(Duration.ofSeconds(10))
        .build()
        .get()
        .uri("download")
        .exchange()
        .expectStatus()
        .isOk();

res.returnResult(DataBuffer.class).getResponseBody().map(DataBufferUtils::release).blockLast();

If I use any other variant, Netty Leak Detector will signal buffer leaks. One such example is as follows

var body = client.mutate()
          .responseTimeout(Duration.ofSeconds(10))
          .build()
          .get()
          .uri("download")
          .exchange()
          .expectStatus()
          .isOk()
          .returnResult()
          .getResponseBodyContent();

assertThat(body).isNotEmpty();

Example above results in netty buffer leaks

2025-12-02T16:32:53.192+01:00 ERROR 11676 --- [demo] [flux-http-nio-3] io.netty.util.ResourceLeakDetector       : LEAK: ByteBuf.release() was not called before it's garbage-collected. See https://netty.io/wiki/reference-counted-objects.html for more information.
Recent access records: 
#1:
	io.netty.handler.codec.http.DefaultHttpContent.release(DefaultHttpContent.java:92)
	io.netty.util.ReferenceCountUtil.release(ReferenceCountUtil.java:90)
	io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:93)
	io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:356)
	io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:434)
	io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346)
	io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:318)
	io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:249)
	io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:354)
	io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1429)
	io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918)
	io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:168)
	io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.handle(AbstractNioChannel.java:445)
	io.netty.channel.nio.NioIoHandler$DefaultNioRegistration.handle(NioIoHandler.java:388)
	io.netty.channel.nio.NioIoHandler.processSelectedKey(NioIoHandler.java:596)
	io.netty.channel.nio.NioIoHandler.processSelectedKeysPlain(NioIoHandler.java:541)
	io.netty.channel.nio.NioIoHandler.processSelectedKeys(NioIoHandler.java:514)
	io.netty.channel.nio.NioIoHandler.run(NioIoHandler.java:484)
	io.netty.channel.SingleThreadIoEventLoop.runIo(SingleThreadIoEventLoop.java:225)
	io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:196)
	io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1193)
	io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	java.base/java.lang.Thread.run(Thread.java:1474)
#2:
	Hint: 'reactor.right.reactiveBridge' will handle the message from this point.
	io.netty.handler.codec.http.DefaultHttpContent.touch(DefaultHttpContent.java:86)
	io.netty.handler.codec.http.DefaultHttpContent.touch(DefaultHttpContent.java:25)
	io.netty.channel.DefaultChannelPipeline.touch(DefaultChannelPipeline.java:115)
	io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:343)
	io.netty.handler.codec.http.HttpContentDecoder.decode(HttpContentDecoder.java:170)
	io.netty.handler.codec.http.HttpContentDecoder.decode(HttpContentDecoder.java:48)
	io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:91)
	io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:356)
	io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:434)
	io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346)
	io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:333)
	io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:455)
	io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290)
	io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:249)
	io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:354)
	io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1429)
	io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918)
	io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:168)
	io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.handle(AbstractNioChannel.java:445)
	io.netty.channel.nio.NioIoHandler$DefaultNioRegistration.handle(NioIoHandler.java:388)
	io.netty.channel.nio.NioIoHandler.processSelectedKey(NioIoHandler.java:596)
	io.netty.channel.nio.NioIoHandler.processSelectedKeysPlain(NioIoHandler.java:541)
	io.netty.channel.nio.NioIoHandler.processSelectedKeys(NioIoHandler.java:514)
	io.netty.channel.nio.NioIoHandler.run(NioIoHandler.java:484)
	io.netty.channel.SingleThreadIoEventLoop.runIo(SingleThreadIoEventLoop.java:225)
	io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:196)
	io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1193)
	io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	java.base/java.lang.Thread.run(Thread.java:1474)
#3:
	org.springframework.core.io.buffer.NettyDataBufferFactory.wrap(NettyDataBufferFactory.java:94)
	org.springframework.http.client.reactive.ReactorClientHttpResponse.lambda$getBody$1(ReactorClientHttpResponse.java:110)
	reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106)
	reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:203)
	reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
	reactor.netty.channel.FluxReceive.onInboundNext(FluxReceive.java:383)
	reactor.netty.channel.ChannelOperations.onInboundNext(ChannelOperations.java:445)
	reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:946)
	reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:115)
	io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:356)
	io.netty.handler.codec.http.HttpContentDecoder.decode(HttpContentDecoder.java:170)
	io.netty.handler.codec.http.HttpContentDecoder.decode(HttpContentDecoder.java:48)
	io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:91)
	io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:356)
	io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:434)
	io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346)
	io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:333)
	io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:455)
	io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290)
	io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:249)
	io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:354)
	io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1429)
	io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918)
	io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:168)
	io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.handle(AbstractNioChannel.java:445)
	io.netty.channel.nio.NioIoHandler$DefaultNioRegistration.handle(NioIoHandler.java:388)
	io.netty.channel.nio.NioIoHandler.processSelectedKey(NioIoHandler.java:596)
	io.netty.channel.nio.NioIoHandler.processSelectedKeysPlain(NioIoHandler.java:541)
	io.netty.channel.nio.NioIoHandler.processSelectedKeys(NioIoHandler.java:514)
	io.netty.channel.nio.NioIoHandler.run(NioIoHandler.java:484)
	io.netty.channel.SingleThreadIoEventLoop.runIo(SingleThreadIoEventLoop.java:225)
	io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:196)
	io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1193)
	io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	java.base/java.lang.Thread.run(Thread.java:1474)
#4:
	io.netty.buffer.AdvancedLeakAwareByteBuf.skipBytes(AdvancedLeakAwareByteBuf.java:539)
	io.netty.handler.codec.http.HttpObjectDecoder.decode(HttpObjectDecoder.java:518)
	io.netty.handler.codec.http.HttpClientCodec$Decoder.decode(HttpClientCodec.java:320)
	io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:530)
	io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:469)
	io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290)
	io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:249)
	io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:354)
	io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1429)
	io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918)
	io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:168)
	io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.handle(AbstractNioChannel.java:445)
	io.netty.channel.nio.NioIoHandler$DefaultNioRegistration.handle(NioIoHandler.java:388)
	io.netty.channel.nio.NioIoHandler.processSelectedKey(NioIoHandler.java:596)
	io.netty.channel.nio.NioIoHandler.processSelectedKeysPlain(NioIoHandler.java:541)
	io.netty.channel.nio.NioIoHandler.processSelectedKeys(NioIoHandler.java:514)
	io.netty.channel.nio.NioIoHandler.run(NioIoHandler.java:484)
	io.netty.channel.SingleThreadIoEventLoop.runIo(SingleThreadIoEventLoop.java:225)
	io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:196)
	io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1193)
	io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	java.base/java.lang.Thread.run(Thread.java:1474)

Full example can be found at https://github.com/krezovic/spring-issues/tree/webtestclient-body-leak

Run with ./mvnw clean verify

Metadata

Metadata

Assignees

Labels

in: testIssues in the test modulestatus: waiting-for-triageAn issue we've not yet triaged or decided on

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions