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

@@ -16,7 +16,6 @@
package org.springframework.web.reactive.function.client;
import java.io.Closeable;
import java.util.List;
import java.util.Optional;
import java.util.OptionalLong;
@@ -44,7 +43,7 @@ import org.springframework.web.reactive.function.BodyExtractor;
* @author Arjen Poutsma
* @since 5.0
*/
public interface ClientResponse extends Closeable {
public interface ClientResponse {
/**
* Return the status code of this response.
@@ -133,18 +132,6 @@ public interface ClientResponse extends Closeable {
*/
<T> Mono<ResponseEntity<List<T>>> toEntityList(ParameterizedTypeReference<T> typeReference);
/**
* Close this response and the underlying HTTP connection.
* <p>This non-blocking method has to be called if its body isn't going
* to be consumed. Not doing so might result in HTTP connection pool
* inconsistencies or memory leaks.
* <p>This shouldn't be called if the response body is read,
* because it would prevent connections to be reused and cancel
* the benefits of using a connection pooling.
*/
@Override
void close();
/**
* Represents the headers of the HTTP response.

View File

@@ -26,6 +26,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -98,32 +99,73 @@ class DefaultClientResponse implements ClientResponse {
@Override
public <T> Mono<T> bodyToMono(Class<? extends T> elementClass) {
return body(BodyExtractors.toMono(elementClass));
if (Void.class.isAssignableFrom(elementClass)) {
return consumeAndCancel();
}
else {
return body(BodyExtractors.toMono(elementClass));
}
}
@SuppressWarnings("unchecked")
private <T> Mono<T> consumeAndCancel() {
return (Mono<T>) this.response.getBody()
.map(buffer -> {
DataBufferUtils.release(buffer);
throw new ReadCancellationException();
})
.onErrorResume(ReadCancellationException.class, ex -> Mono.empty())
.then();
}
@Override
public <T> Mono<T> bodyToMono(ParameterizedTypeReference<T> typeReference) {
return body(BodyExtractors.toMono(typeReference));
if (Void.class.isAssignableFrom(typeReference.getType().getClass())) {
return consumeAndCancel();
}
else {
return body(BodyExtractors.toMono(typeReference));
}
}
@Override
public <T> Flux<T> bodyToFlux(Class<? extends T> elementClass) {
return body(BodyExtractors.toFlux(elementClass));
if (Void.class.isAssignableFrom(elementClass)) {
return Flux.from(consumeAndCancel());
}
else {
return body(BodyExtractors.toFlux(elementClass));
}
}
@Override
public <T> Flux<T> bodyToFlux(ParameterizedTypeReference<T> typeReference) {
return body(BodyExtractors.toFlux(typeReference));
if (Void.class.isAssignableFrom(typeReference.getType().getClass())) {
return Flux.from(consumeAndCancel());
}
else {
return body(BodyExtractors.toFlux(typeReference));
}
}
@Override
public <T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType) {
return toEntityInternal(bodyToMono(bodyType));
if (Void.class.isAssignableFrom(bodyType)) {
return toEntityInternal(consumeAndCancel());
}
else {
return toEntityInternal(bodyToMono(bodyType));
}
}
@Override
public <T> Mono<ResponseEntity<T>> toEntity(ParameterizedTypeReference<T> typeReference) {
return toEntityInternal(bodyToMono(typeReference));
if (Void.class.isAssignableFrom(typeReference.getType().getClass())) {
return toEntityInternal(consumeAndCancel());
}
else {
return toEntityInternal(bodyToMono(typeReference));
}
}
private <T> Mono<ResponseEntity<T>> toEntityInternal(Mono<T> bodyMono) {
@@ -154,10 +196,6 @@ class DefaultClientResponse implements ClientResponse {
.map(body -> new ResponseEntity<>(body, headers, statusCode));
}
@Override
public void close() {
this.response.close();
}
private class DefaultHeaders implements Headers {
@@ -191,4 +229,8 @@ class DefaultClientResponse implements ClientResponse {
}
}
@SuppressWarnings("serial")
private class ReadCancellationException extends RuntimeException {
}
}

View File

@@ -461,17 +461,11 @@ public interface WebClient {
* .exchange()
* .flatMapMany(response -> response.bodyToFlux(Pojo.class));
* </pre>
* <p>If the response body is not consumed with {@code bodyTo*}
* or {@code toEntity*} methods, it is your responsibility
* to release the HTTP resources with {@link ClientResponse#close()}.
* <pre>
* Mono&lt;HttpStatus&gt; mono = client.get().uri("/")
* .exchange()
* .map(response -> {
* response.close();
* return response.statusCode();
* });
* </pre>
* <p>The response body should always be consumed with {@code bodyTo*}
* or {@code toEntity*} methods; if you do not care about the body,
* you can use {@code bodyToMono(Void.class)}.
* <p>Not consuming the response body might lead to HTTP connection pool
* inconsistencies or memory leaks.
* @return a {@code Mono} with the response
* @see #retrieve()
*/
@@ -491,8 +485,6 @@ public interface WebClient {
* .retrieve()
* .bodyToMono(Pojo.class);
* </pre>
* <p>Since this method reads the response body,
* {@link ClientResponse#close()} should not be called.
* @return spec with options for extracting the response body
*/
ResponseSpec retrieve();