Skip to content

Commit 7418c4b

Browse files
committed
Fix buffer leak in AbstractServerHttpResponse
See gh-26232
1 parent ad42010 commit 7418c4b

File tree

2 files changed

+39
-3
lines changed

2 files changed

+39
-3
lines changed

spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,16 @@ public final Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
211211
// We must resolve value first however, for a chance to handle potential error.
212212
if (body instanceof Mono) {
213213
return ((Mono<? extends DataBuffer>) body)
214-
.flatMap(buffer -> doCommit(() ->
215-
writeWithInternal(Mono.fromCallable(() -> buffer)
216-
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release))))
214+
.flatMap(buffer ->
215+
doCommit(() -> {
216+
try {
217+
return writeWithInternal(Mono.fromCallable(() -> buffer)
218+
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release));
219+
}
220+
catch (Throwable ex) {
221+
return Mono.error(ex);
222+
}
223+
}).doOnError(ex -> DataBufferUtils.release(buffer)))
217224
.doOnError(t -> getHeaders().clearContentHeaders());
218225
}
219226
else {

spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,33 @@
1919
import java.nio.ByteBuffer;
2020
import java.nio.charset.StandardCharsets;
2121
import java.util.ArrayList;
22+
import java.util.Collections;
2223
import java.util.List;
24+
import java.util.Map;
2325
import java.util.function.Consumer;
2426
import java.util.function.Supplier;
2527

2628
import org.junit.jupiter.api.Test;
2729
import org.reactivestreams.Publisher;
2830
import reactor.core.publisher.Flux;
2931
import reactor.core.publisher.Mono;
32+
import reactor.netty.channel.AbortedException;
3033
import reactor.test.StepVerifier;
3134

35+
import org.springframework.core.ResolvableType;
3236
import org.springframework.core.io.buffer.DataBuffer;
3337
import org.springframework.core.io.buffer.DefaultDataBuffer;
3438
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
39+
import org.springframework.core.testfixture.io.buffer.LeakAwareDataBufferFactory;
3540
import org.springframework.http.HttpHeaders;
3641
import org.springframework.http.HttpStatus;
3742
import org.springframework.http.MediaType;
3843
import org.springframework.http.ResponseCookie;
44+
import org.springframework.http.codec.EncoderHttpMessageWriter;
45+
import org.springframework.http.codec.HttpMessageWriter;
46+
import org.springframework.http.codec.json.Jackson2JsonEncoder;
47+
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
48+
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse;
3949

4050
import static org.assertj.core.api.Assertions.assertThat;
4151

@@ -186,6 +196,25 @@ void beforeCommitErrorShouldLeaveResponseNotCommitted() {
186196
});
187197
}
188198

199+
@Test // gh-26232
200+
void monoResponseShouldNotLeakIfCancelled() {
201+
LeakAwareDataBufferFactory bufferFactory = new LeakAwareDataBufferFactory();
202+
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
203+
MockServerHttpResponse response = new MockServerHttpResponse(bufferFactory);
204+
response.setWriteHandler(flux -> {
205+
throw AbortedException.beforeSend();
206+
});
207+
208+
HttpMessageWriter<Object> messageWriter = new EncoderHttpMessageWriter<>(new Jackson2JsonEncoder());
209+
Mono<Void> result = messageWriter.write(Mono.just(Collections.singletonMap("foo", "bar")),
210+
ResolvableType.forClass(Mono.class), ResolvableType.forClass(Map.class), null,
211+
request, response, Collections.emptyMap());
212+
213+
StepVerifier.create(result).expectError(AbortedException.class).verify();
214+
215+
bufferFactory.checkForLeaks();
216+
}
217+
189218

190219
private DefaultDataBuffer wrap(String a) {
191220
return DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(a.getBytes(StandardCharsets.UTF_8)));

0 commit comments

Comments
 (0)