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);