Fix memory leak in WiretapConnector

Prior to this commit, we found in gh-35953 that using the `WebTestClient`
the following way leaks data buffers:

```
var body = client.get().uri("download")
  .exchange()
  .expectStatus().isOk()
  .returnResult()
  .getResponseBodyContent();
```

Here, the test performs expectations on the response status and headers,
but not on the response body. The WiretapConnector already supports this
case by subscribing to the Flux response body in those cases and
accumulating the entire content as a single byte[].

Here, the `DataBuffer` instances are not decoded by any `Decoder` and
are not released. This results in a memory leak.

This commit ensures that the automatic subscription in
`WiretapConnector` also releases the buffers automatically as the DSL
does not allow at that point to go back to performing body expectations.

Fixes gh-36050
This commit is contained in:
Brian Clozel
2025-12-19 16:07:39 +01:00
parent f1db0ef036
commit 7353ab41d2
2 changed files with 76 additions and 6 deletions
@@ -31,6 +31,7 @@ import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.ClientHttpConnector;
@@ -196,11 +197,21 @@ class WiretapConnector implements ClientHttpConnector {
// 1. Mock server never consumed request body (for example, error before read)
// 2. FluxExchangeResult: getResponseBodyContent called before getResponseBody
//noinspection ConstantConditions
(this.publisher != null ? this.publisher : this.publisherNested)
.onErrorMap(ex -> new IllegalStateException(
"Content has not been consumed, and " +
"an error was raised while attempting to produce it.", ex))
.subscribe();
if (this.publisher != null) {
this.publisher.doOnNext(DataBufferUtils::release)
.onErrorMap(ex -> new IllegalStateException(
"Content has not been consumed, and " +
"an error was raised while attempting to produce it.", ex))
.subscribe();
}
else if (this.publisherNested != null) {
this.publisherNested
.map(pub -> Flux.from(pub).doOnNext(DataBufferUtils::release))
.onErrorMap(ex -> new IllegalStateException(
"Content has not been consumed, and " +
"an error was raised while attempting to produce it.", ex))
.subscribe();
}
}
return this.content.asMono();
});