From 37398c669ca3af857487d963f3645e6d1cb678e5 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 2 Sep 2019 12:25:18 +0200 Subject: [PATCH] Add toBodilessEntity to ClientResponse and WebClient.ResponseSpec See gh-23498 --- .../function/client/ClientResponse.java | 10 +++ .../client/DefaultClientResponse.java | 6 ++ .../function/client/DefaultWebClient.java | 8 ++ .../reactive/function/client/WebClient.java | 12 +++ .../function/client/WebClientUtils.java | 5 +- .../client/support/ClientResponseWrapper.java | 5 ++ .../WebClientDataBufferAllocatingTests.java | 25 ++++++ .../client/WebClientIntegrationTests.java | 79 +++++++++++++++++++ 8 files changed, 147 insertions(+), 3 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java index 428b14e098..cb7c9cd075 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java @@ -59,6 +59,7 @@ import org.springframework.web.reactive.function.BodyExtractor; * {@link #toEntity(ParameterizedTypeReference)} *
  • {@link #toEntityList(Class)} or * {@link #toEntityList(ParameterizedTypeReference)}
  • +*
  • {@link #toBodilessEntity()}
  • *
  • {@link #releaseBody()}
  • * * You can use {@code bodyToMono(Void.class)} if no response content is @@ -184,6 +185,15 @@ public interface ClientResponse { */ Mono>> toEntityList(ParameterizedTypeReference 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> toBodilessEntity(); + /** * Creates a {@link WebClientResponseException} based on the status code, * headers, and body of this response as well as the corresponding request. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index 18eff19e79..11d64b9f7f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -162,6 +162,12 @@ class DefaultClientResponse implements ClientResponse { .then(); } + @Override + public Mono> toBodilessEntity() { + return releaseBody() + .then(WebClientUtils.toEntity(this, Mono.empty())); + } + @Override public Mono> toEntity(Class bodyType) { return WebClientUtils.toEntity(this, bodyToMono(bodyType)); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index 0bbdb93330..0f958e4bf7 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -562,6 +562,14 @@ class DefaultWebClient implements WebClient { handleBodyFlux(response, response.bodyToFlux(elementTypeRef)))); } + @Override + public Mono> toBodilessEntity() { + return this.responseMono.flatMap(response -> + WebClientUtils.toEntity(response, handleBodyMono(response, Mono.empty())) + .doOnNext(entity -> response.releaseBody()) // body is drained in other cases + ); + } + private static class StatusHandler { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index 0401b4be81..f20b85f0e2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -751,6 +751,18 @@ public interface WebClient { * @since 5.2 */ Mono>> toEntityList(ParameterizedTypeReference 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> toBodilessEntity(); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java index e68c220080..89d4b400eb 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java @@ -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 ResponseEntity createEntity(@Nullable T body, HttpHeaders headers, int status) { + public static ResponseEntity createEntity(@Nullable T body, HttpHeaders headers, int status) { HttpStatus resolvedStatus = HttpStatus.resolve(status); return resolvedStatus != null ? new ResponseEntity<>(body, headers, resolvedStatus) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapper.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapper.java index 045e0bd6b0..8c98310ce5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapper.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapper.java @@ -123,6 +123,11 @@ public class ClientResponseWrapper implements ClientResponse { return this.delegate.releaseBody(); } + @Override + public Mono> toBodilessEntity() { + return this.delegate.toBodilessEntity(); + } + @Override public Mono> toEntity(Class bodyType) { return this.delegate.toEntity(bodyType); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java index 397ae88102..4eb6ed1705 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java @@ -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> 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> exceptionFunction) { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index 65315c2db2..15cf8d6b27 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -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> 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> 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> 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);