Skip to content

Commit 37398c6

Browse files
committed
Add toBodilessEntity to ClientResponse and WebClient.ResponseSpec
See gh-23498
1 parent f5640cb commit 37398c6

File tree

8 files changed

+147
-3
lines changed

8 files changed

+147
-3
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
* {@link #toEntity(ParameterizedTypeReference)}</li>
6060
* <li>{@link #toEntityList(Class)} or
6161
* {@link #toEntityList(ParameterizedTypeReference)}</li>
62+
* <li>{@link #toBodilessEntity()}</li>
6263
* <li>{@link #releaseBody()}</li>
6364
* </ul>
6465
* You can use {@code bodyToMono(Void.class)} if no response content is
@@ -184,6 +185,15 @@ public interface ClientResponse {
184185
*/
185186
<T> Mono<ResponseEntity<List<T>>> toEntityList(ParameterizedTypeReference<T> elementTypeRef);
186187

188+
/**
189+
* Return this response as a delayed {@code ResponseEntity} containing
190+
* status and headers, but no body. Calling this method will
191+
* {@linkplain #releaseBody() release} the body of the response.
192+
* @return {@code Mono} with the bodiless {@code ResponseEntity}
193+
* @since 5.2
194+
*/
195+
Mono<ResponseEntity<Void>> toBodilessEntity();
196+
187197
/**
188198
* Creates a {@link WebClientResponseException} based on the status code,
189199
* headers, and body of this response as well as the corresponding request.

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ public Mono<Void> releaseBody() {
162162
.then();
163163
}
164164

165+
@Override
166+
public Mono<ResponseEntity<Void>> toBodilessEntity() {
167+
return releaseBody()
168+
.then(WebClientUtils.toEntity(this, Mono.empty()));
169+
}
170+
165171
@Override
166172
public <T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType) {
167173
return WebClientUtils.toEntity(this, bodyToMono(bodyType));

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,14 @@ public <T> Mono<ResponseEntity<List<T>>> toEntityList(ParameterizedTypeReference
562562
handleBodyFlux(response, response.bodyToFlux(elementTypeRef))));
563563
}
564564

565+
@Override
566+
public Mono<ResponseEntity<Void>> toBodilessEntity() {
567+
return this.responseMono.flatMap(response ->
568+
WebClientUtils.toEntity(response, handleBodyMono(response, Mono.<Void>empty()))
569+
.doOnNext(entity -> response.releaseBody()) // body is drained in other cases
570+
);
571+
}
572+
565573

566574
private static class StatusHandler {
567575

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,18 @@ ResponseSpec onRawStatus(IntPredicate statusCodePredicate,
751751
* @since 5.2
752752
*/
753753
<T> Mono<ResponseEntity<List<T>>> toEntityList(ParameterizedTypeReference<T> elementTypeRef);
754+
755+
/**
756+
* Return the response as a delayed {@code ResponseEntity} containing status and headers,
757+
* but no body. By default, if the response has status code 4xx or 5xx, the {@code Mono}
758+
* will contain a {@link WebClientException}. This can be overridden with
759+
* {@link #onStatus(Predicate, Function)}.
760+
* Calling this method will {@linkplain ClientResponse#releaseBody() release} the body of
761+
* the response.
762+
* @return {@code Mono} with the bodiless {@code ResponseEntity}
763+
* @since 5.2
764+
*/
765+
Mono<ResponseEntity<Void>> toBodilessEntity();
754766
}
755767

756768

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ public static <T> Mono<ResponseEntity<T>> toEntity(ClientResponse response, Mon
4444
int status = response.rawStatusCode();
4545
return bodyMono
4646
.map(body -> createEntity(body, headers, status))
47-
.switchIfEmpty(Mono.defer(
48-
() -> Mono.just(createEntity(null, headers, status))));
47+
.switchIfEmpty(Mono.fromCallable( () -> createEntity(null, headers, status)));
4948
});
5049
}
5150

@@ -62,7 +61,7 @@ public static <T> Mono<ResponseEntity<List<T>>> toEntityList(ClientResponse resp
6261
});
6362
}
6463

65-
private static <T> ResponseEntity<T> createEntity(@Nullable T body, HttpHeaders headers, int status) {
64+
public static <T> ResponseEntity<T> createEntity(@Nullable T body, HttpHeaders headers, int status) {
6665
HttpStatus resolvedStatus = HttpStatus.resolve(status);
6766
return resolvedStatus != null
6867
? new ResponseEntity<>(body, headers, resolvedStatus)

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapper.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ public Mono<Void> releaseBody() {
123123
return this.delegate.releaseBody();
124124
}
125125

126+
@Override
127+
public Mono<ResponseEntity<Void>> toBodilessEntity() {
128+
return this.delegate.toBodilessEntity();
129+
}
130+
126131
@Override
127132
public <T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType) {
128133
return this.delegate.toEntity(bodyType);

spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.web.reactive.function.client;
1717

1818
import java.time.Duration;
19+
import java.util.Collections;
1920
import java.util.Map;
2021
import java.util.function.Function;
2122

@@ -34,6 +35,7 @@
3435
import org.springframework.core.io.buffer.NettyDataBufferFactory;
3536
import org.springframework.http.HttpStatus;
3637
import org.springframework.http.MediaType;
38+
import org.springframework.http.ResponseEntity;
3739
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
3840
import org.springframework.http.client.reactive.ReactorResourceFactory;
3941
import org.springframework.web.reactive.function.UnsupportedMediaTypeException;
@@ -185,6 +187,29 @@ public void releaseBody(String displayName, DataBufferFactory bufferFactory) {
185187
.verify(Duration.ofSeconds(3));
186188
}
187189

190+
@ParameterizedDataBufferAllocatingTest
191+
public void exchangeToBodilessEntity(String displayName, DataBufferFactory bufferFactory) {
192+
super.bufferFactory = bufferFactory;
193+
194+
this.server.enqueue(new MockResponse()
195+
.setResponseCode(201)
196+
.setHeader("Foo", "bar")
197+
.setBody("foo bar"));
198+
199+
Mono<ResponseEntity<Void>> result = this.webClient.get()
200+
.exchange()
201+
.flatMap(ClientResponse::toBodilessEntity);
202+
203+
StepVerifier.create(result)
204+
.assertNext(entity -> {
205+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CREATED);
206+
assertThat(entity.getHeaders()).containsEntry("Foo", Collections.singletonList("bar"));
207+
assertThat(entity.getBody()).isNull();
208+
})
209+
.expectComplete()
210+
.verify(Duration.ofSeconds(3));
211+
}
212+
188213

189214
private void testOnStatus(Throwable expected,
190215
Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction) {

spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,62 @@ void retrieveShouldReceiveJsonAsResponseEntityString(ClientHttpConnector connect
291291
});
292292
}
293293

294+
@ParameterizedWebClientTest
295+
void exchangeBodilessEntity(ClientHttpConnector connector) {
296+
startServer(connector);
297+
298+
prepareResponse(response -> response
299+
.setHeader("Content-Type", "application/json").setBody("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"));
300+
301+
Mono<ResponseEntity<Void>> result = this.webClient.get()
302+
.uri("/json").accept(MediaType.APPLICATION_JSON)
303+
.exchange()
304+
.flatMap(ClientResponse::toBodilessEntity);
305+
306+
StepVerifier.create(result)
307+
.consumeNextWith(entity -> {
308+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
309+
assertThat(entity.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
310+
assertThat(entity.getHeaders().getContentLength()).isEqualTo(31);
311+
assertThat(entity.getBody()).isNull();
312+
})
313+
.expectComplete().verify(Duration.ofSeconds(3));
314+
315+
expectRequestCount(1);
316+
expectRequest(request -> {
317+
assertThat(request.getPath()).isEqualTo("/json");
318+
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
319+
});
320+
}
321+
322+
@ParameterizedWebClientTest
323+
void retrieveBodilessEntity(ClientHttpConnector connector) {
324+
startServer(connector);
325+
326+
prepareResponse(response -> response
327+
.setHeader("Content-Type", "application/json").setBody("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"));
328+
329+
Mono<ResponseEntity<Void>> result = this.webClient.get()
330+
.uri("/json").accept(MediaType.APPLICATION_JSON)
331+
.retrieve()
332+
.toBodilessEntity();
333+
334+
StepVerifier.create(result)
335+
.consumeNextWith(entity -> {
336+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
337+
assertThat(entity.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
338+
assertThat(entity.getHeaders().getContentLength()).isEqualTo(31);
339+
assertThat(entity.getBody()).isNull();
340+
})
341+
.expectComplete().verify(Duration.ofSeconds(3));
342+
343+
expectRequestCount(1);
344+
expectRequest(request -> {
345+
assertThat(request.getPath()).isEqualTo("/json");
346+
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
347+
});
348+
}
349+
294350
@ParameterizedWebClientTest
295351
void retrieveEntityWithServerError(ClientHttpConnector connector) {
296352
startServer(connector);
@@ -314,6 +370,29 @@ void retrieveEntityWithServerError(ClientHttpConnector connector) {
314370
});
315371
}
316372

373+
@ParameterizedWebClientTest
374+
void retrieveBodilessEntityWithServerError(ClientHttpConnector connector) {
375+
startServer(connector);
376+
377+
prepareResponse(response -> response.setResponseCode(500)
378+
.setHeader("Content-Type", "text/plain").setBody("Internal Server error"));
379+
380+
Mono<ResponseEntity<Void>> result = this.webClient.get()
381+
.uri("/").accept(MediaType.APPLICATION_JSON)
382+
.retrieve()
383+
.toBodilessEntity();
384+
385+
StepVerifier.create(result)
386+
.expectError(WebClientResponseException.class)
387+
.verify(Duration.ofSeconds(3));
388+
389+
expectRequestCount(1);
390+
expectRequest(request -> {
391+
assertThat(request.getPath()).isEqualTo("/");
392+
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
393+
});
394+
}
395+
317396
@ParameterizedWebClientTest
318397
void retrieveEntityWithServerErrorStatusHandler(ClientHttpConnector connector) {
319398
startServer(connector);

0 commit comments

Comments
 (0)