Add toBodilessEntity to ClientResponse and WebClient.ResponseSpec

See gh-23498
This commit is contained in:
Arjen Poutsma
2019-09-02 12:25:18 +02:00
parent f5640cbfe0
commit 37398c669c
8 changed files with 147 additions and 3 deletions

View File

@@ -59,6 +59,7 @@ import org.springframework.web.reactive.function.BodyExtractor;
* {@link #toEntity(ParameterizedTypeReference)}</li>
* <li>{@link #toEntityList(Class)} or
* {@link #toEntityList(ParameterizedTypeReference)}</li>
* <li>{@link #toBodilessEntity()}</li>
* <li>{@link #releaseBody()}</li>
* </ul>
* You can use {@code bodyToMono(Void.class)} if no response content is
@@ -184,6 +185,15 @@ public interface ClientResponse {
*/
<T> Mono<ResponseEntity<List<T>>> toEntityList(ParameterizedTypeReference<T> elementTypeRef);
/**
* Return this response as a delayed {@code ResponseEntity} containing
* status and headers, but no body. Calling this method will
* {@linkplain #releaseBody() release} the body of the response.
* @return {@code Mono} with the bodiless {@code ResponseEntity}
* @since 5.2
*/
Mono<ResponseEntity<Void>> toBodilessEntity();
/**
* Creates a {@link WebClientResponseException} based on the status code,
* headers, and body of this response as well as the corresponding request.

View File

@@ -162,6 +162,12 @@ class DefaultClientResponse implements ClientResponse {
.then();
}
@Override
public Mono<ResponseEntity<Void>> toBodilessEntity() {
return releaseBody()
.then(WebClientUtils.toEntity(this, Mono.empty()));
}
@Override
public <T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType) {
return WebClientUtils.toEntity(this, bodyToMono(bodyType));

View File

@@ -562,6 +562,14 @@ class DefaultWebClient implements WebClient {
handleBodyFlux(response, response.bodyToFlux(elementTypeRef))));
}
@Override
public Mono<ResponseEntity<Void>> toBodilessEntity() {
return this.responseMono.flatMap(response ->
WebClientUtils.toEntity(response, handleBodyMono(response, Mono.<Void>empty()))
.doOnNext(entity -> response.releaseBody()) // body is drained in other cases
);
}
private static class StatusHandler {

View File

@@ -751,6 +751,18 @@ public interface WebClient {
* @since 5.2
*/
<T> Mono<ResponseEntity<List<T>>> toEntityList(ParameterizedTypeReference<T> elementTypeRef);
/**
* Return the response as a delayed {@code ResponseEntity} containing status and headers,
* but no body. By default, if the response has status code 4xx or 5xx, the {@code Mono}
* will contain a {@link WebClientException}. This can be overridden with
* {@link #onStatus(Predicate, Function)}.
* Calling this method will {@linkplain ClientResponse#releaseBody() release} the body of
* the response.
* @return {@code Mono} with the bodiless {@code ResponseEntity}
* @since 5.2
*/
Mono<ResponseEntity<Void>> toBodilessEntity();
}

View File

@@ -44,8 +44,7 @@ abstract class WebClientUtils {
int status = response.rawStatusCode();
return bodyMono
.map(body -> createEntity(body, headers, status))
.switchIfEmpty(Mono.defer(
() -> Mono.just(createEntity(null, headers, status))));
.switchIfEmpty(Mono.fromCallable( () -> createEntity(null, headers, status)));
});
}
@@ -62,7 +61,7 @@ abstract class WebClientUtils {
});
}
private static <T> ResponseEntity<T> createEntity(@Nullable T body, HttpHeaders headers, int status) {
public static <T> ResponseEntity<T> createEntity(@Nullable T body, HttpHeaders headers, int status) {
HttpStatus resolvedStatus = HttpStatus.resolve(status);
return resolvedStatus != null
? new ResponseEntity<>(body, headers, resolvedStatus)

View File

@@ -123,6 +123,11 @@ public class ClientResponseWrapper implements ClientResponse {
return this.delegate.releaseBody();
}
@Override
public Mono<ResponseEntity<Void>> toBodilessEntity() {
return this.delegate.toBodilessEntity();
}
@Override
public <T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType) {
return this.delegate.toEntity(bodyType);

View File

@@ -16,6 +16,7 @@
package org.springframework.web.reactive.function.client;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
@@ -34,6 +35,7 @@ import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.client.reactive.ReactorResourceFactory;
import org.springframework.web.reactive.function.UnsupportedMediaTypeException;
@@ -185,6 +187,29 @@ public class WebClientDataBufferAllocatingTests extends AbstractDataBufferAlloca
.verify(Duration.ofSeconds(3));
}
@ParameterizedDataBufferAllocatingTest
public void exchangeToBodilessEntity(String displayName, DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
this.server.enqueue(new MockResponse()
.setResponseCode(201)
.setHeader("Foo", "bar")
.setBody("foo bar"));
Mono<ResponseEntity<Void>> result = this.webClient.get()
.exchange()
.flatMap(ClientResponse::toBodilessEntity);
StepVerifier.create(result)
.assertNext(entity -> {
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(entity.getHeaders()).containsEntry("Foo", Collections.singletonList("bar"));
assertThat(entity.getBody()).isNull();
})
.expectComplete()
.verify(Duration.ofSeconds(3));
}
private void testOnStatus(Throwable expected,
Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction) {

View File

@@ -291,6 +291,62 @@ class WebClientIntegrationTests {
});
}
@ParameterizedWebClientTest
void exchangeBodilessEntity(ClientHttpConnector connector) {
startServer(connector);
prepareResponse(response -> response
.setHeader("Content-Type", "application/json").setBody("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"));
Mono<ResponseEntity<Void>> result = this.webClient.get()
.uri("/json").accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(ClientResponse::toBodilessEntity);
StepVerifier.create(result)
.consumeNextWith(entity -> {
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
assertThat(entity.getHeaders().getContentLength()).isEqualTo(31);
assertThat(entity.getBody()).isNull();
})
.expectComplete().verify(Duration.ofSeconds(3));
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/json");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
});
}
@ParameterizedWebClientTest
void retrieveBodilessEntity(ClientHttpConnector connector) {
startServer(connector);
prepareResponse(response -> response
.setHeader("Content-Type", "application/json").setBody("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"));
Mono<ResponseEntity<Void>> result = this.webClient.get()
.uri("/json").accept(MediaType.APPLICATION_JSON)
.retrieve()
.toBodilessEntity();
StepVerifier.create(result)
.consumeNextWith(entity -> {
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
assertThat(entity.getHeaders().getContentLength()).isEqualTo(31);
assertThat(entity.getBody()).isNull();
})
.expectComplete().verify(Duration.ofSeconds(3));
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/json");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
});
}
@ParameterizedWebClientTest
void retrieveEntityWithServerError(ClientHttpConnector connector) {
startServer(connector);
@@ -314,6 +370,29 @@ class WebClientIntegrationTests {
});
}
@ParameterizedWebClientTest
void retrieveBodilessEntityWithServerError(ClientHttpConnector connector) {
startServer(connector);
prepareResponse(response -> response.setResponseCode(500)
.setHeader("Content-Type", "text/plain").setBody("Internal Server error"));
Mono<ResponseEntity<Void>> result = this.webClient.get()
.uri("/").accept(MediaType.APPLICATION_JSON)
.retrieve()
.toBodilessEntity();
StepVerifier.create(result)
.expectError(WebClientResponseException.class)
.verify(Duration.ofSeconds(3));
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
});
}
@ParameterizedWebClientTest
void retrieveEntityWithServerErrorStatusHandler(ClientHttpConnector connector) {
startServer(connector);