Fix behavior of ClientResponse#bodyTo** with Void

Prior to this commit, asking for a `Void` type using any of the
`ClientResponse#bodyTo*` methods would immediately return an empty
`Publisher` without consuming the response body.

Not doing so can lead to HTTP connection pool inconsistencies and/or
memory leaks, since:

* a connection that still has a response body being written to it cannot
be properly recycled in the connection pool
* incoming `DataBuffer` might not be released

This commit detects when `Void` types are asked as body types and in
those cases does the following:

1. Subscribe to the response body `Publisher` to allow the connection to
be returned to the connection pool
2. `cancel()` the body `Publisher` if the response body is not empty; in
that case, we choose to close the connection vs. consume the whole
response body

Those changes imply that `ClientHttpResponse` and other related
contracts don't need a `close()` method anymore.

Issue: SPR-16018
This commit is contained in:
Brian Clozel
2017-09-27 23:08:30 +02:00
parent ec345bf162
commit 126ac849e5
11 changed files with 122 additions and 152 deletions

View File

@@ -28,6 +28,8 @@ import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.test.publisher.TestPublisher;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.codec.StringDecoder;
@@ -46,8 +48,10 @@ import org.springframework.http.codec.HttpMessageReader;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
/**
@@ -214,7 +218,8 @@ public class DefaultClientResponseTests {
when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders);
Flux<String> resultFlux =
defaultClientResponse.bodyToFlux(new ParameterizedTypeReference<String>() {});
defaultClientResponse.bodyToFlux(new ParameterizedTypeReference<String>() {
});
Mono<List<String>> result = resultFlux.collectList();
assertEquals(Collections.singletonList("foo"), result.block());
}
@@ -260,7 +265,8 @@ public class DefaultClientResponseTests {
when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders);
ResponseEntity<String> result = defaultClientResponse.toEntity(
new ParameterizedTypeReference<String>() {}).block();
new ParameterizedTypeReference<String>() {
}).block();
assertEquals("foo", result.getBody());
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals(MediaType.TEXT_PLAIN, result.getHeaders().getContentType());
@@ -307,13 +313,60 @@ public class DefaultClientResponseTests {
when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders);
ResponseEntity<List<String>> result = defaultClientResponse.toEntityList(
new ParameterizedTypeReference<String>() {}).block();
new ParameterizedTypeReference<String>() {
}).block();
assertEquals(Collections.singletonList("foo"), result.getBody());
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals(MediaType.TEXT_PLAIN, result.getHeaders().getContentType());
}
@Test
public void toMonoVoid() throws Exception {
TestPublisher<DataBuffer> body = TestPublisher.create();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
when(mockResponse.getHeaders()).thenReturn(httpHeaders);
when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK);
when(mockResponse.getBody()).thenReturn(body.flux());
List<HttpMessageReader<?>> messageReaders = Collections
.singletonList(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true)));
when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders);
StepVerifier.create(defaultClientResponse.bodyToMono(Void.class))
.then(() -> {
body.assertWasSubscribed();
body.complete();
})
.verifyComplete();
}
@Test
public void toMonoVoidNonEmptyBody() throws Exception {
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
DefaultDataBuffer dataBuffer =
factory.wrap(ByteBuffer.wrap("foo".getBytes(StandardCharsets.UTF_8)));
TestPublisher<DataBuffer> body = TestPublisher.create();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
when(mockResponse.getHeaders()).thenReturn(httpHeaders);
when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK);
when(mockResponse.getBody()).thenReturn(body.flux());
List<HttpMessageReader<?>> messageReaders = Collections
.singletonList(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true)));
when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders);
StepVerifier.create(defaultClientResponse.bodyToMono(Void.class))
.then(() -> {
body.assertWasSubscribed();
body.emit(dataBuffer);
})
.verifyComplete();
body.assertCancelled();
}
}

View File

@@ -541,15 +541,15 @@ public class WebClientIntegrationTests {
@Test
public void shouldReceiveEmptyResponse() throws Exception {
prepareResponse(response -> response.setHeader("Content-Length", "0"));
prepareResponse(response -> response.setHeader("Content-Length", "0").setBody(""));
Mono<ClientResponse> result = this.webClient.get()
Mono<ResponseEntity<Void>> result = this.webClient.get()
.uri("/noContent")
.exchange();
.exchange()
.flatMap(response -> response.toEntity(Void.class));
StepVerifier.create(result).assertNext(r -> {
assertTrue(r.statusCode().is2xxSuccessful());
StepVerifier.create(r.bodyToMono(Void.class)).verifyComplete();
assertTrue(r.getStatusCode().is2xxSuccessful());
}).verifyComplete();
}

View File

@@ -1,53 +0,0 @@
package org.springframework.web.reactive.function.client;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.mock.http.client.reactive.test.MockClientHttpResponse;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Mock tests using a {@link ExchangeFunction} through {@link WebClient}.
*
* @author Brian Clozel
*/
public class WebClientMockTests {
private MockClientHttpResponse response;
private ClientHttpConnector mockConnector;
private WebClient webClient;
@Before
public void setUp() throws Exception {
this.mockConnector = mock(ClientHttpConnector.class);
this.webClient = WebClient.builder().clientConnector(this.mockConnector).build();
this.response = new MockClientHttpResponse(HttpStatus.OK);
this.response.setBody("example");
given(this.mockConnector.connect(any(), any(), any())).willReturn(Mono.just(this.response));
}
@Test
public void shouldDisposeResponseManually() {
Mono<HttpHeaders> headers = this.webClient
.get().uri("/test")
.exchange()
.map(response -> response.headers().asHttpHeaders());
StepVerifier.create(headers)
.expectNextCount(1)
.verifyComplete();
assertFalse(this.response.isClosed());
}
}