Skip to content

Commit d883815

Browse files
committed
Copy ClientResponseEntity::toEntity* methods to ResponseSpec
This commit copies the toEntity and toEntityList methods from ClientResponse to ResponseSpec, so that it is possible to retrieve a ResponseEntity when using retrieve(). Closes gh-22368
1 parent 38f6d27 commit d883815

File tree

5 files changed

+274
-38
lines changed

5 files changed

+274
-38
lines changed

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

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -157,31 +157,22 @@ public <T> Flux<T> bodyToFlux(ParameterizedTypeReference<T> elementTypeRef) {
157157

158158
@Override
159159
public <T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType) {
160-
return toEntityInternal(bodyToMono(bodyType));
160+
return WebClientUtils.toEntity(this, bodyToMono(bodyType));
161161
}
162162

163163
@Override
164164
public <T> Mono<ResponseEntity<T>> toEntity(ParameterizedTypeReference<T> bodyTypeReference) {
165-
return toEntityInternal(bodyToMono(bodyTypeReference));
166-
}
167-
168-
private <T> Mono<ResponseEntity<T>> toEntityInternal(Mono<T> bodyMono) {
169-
HttpHeaders headers = headers().asHttpHeaders();
170-
int status = rawStatusCode();
171-
return bodyMono
172-
.map(body -> createEntity(body, headers, status))
173-
.switchIfEmpty(Mono.defer(
174-
() -> Mono.just(createEntity(headers, status))));
165+
return WebClientUtils.toEntity(this, bodyToMono(bodyTypeReference));
175166
}
176167

177168
@Override
178169
public <T> Mono<ResponseEntity<List<T>>> toEntityList(Class<T> elementClass) {
179-
return toEntityListInternal(bodyToFlux(elementClass));
170+
return WebClientUtils.toEntityList(this, bodyToFlux(elementClass));
180171
}
181172

182173
@Override
183174
public <T> Mono<ResponseEntity<List<T>>> toEntityList(ParameterizedTypeReference<T> elementTypeRef) {
184-
return toEntityListInternal(bodyToFlux(elementTypeRef));
175+
return WebClientUtils.toEntityList(this, bodyToFlux(elementTypeRef));
185176
}
186177

187178
@Override
@@ -224,29 +215,6 @@ HttpRequest request() {
224215
return this.requestSupplier.get();
225216
}
226217

227-
private <T> Mono<ResponseEntity<List<T>>> toEntityListInternal(Flux<T> bodyFlux) {
228-
HttpHeaders headers = headers().asHttpHeaders();
229-
int status = rawStatusCode();
230-
return bodyFlux
231-
.collectList()
232-
.map(body -> createEntity(body, headers, status));
233-
}
234-
235-
private <T> ResponseEntity<T> createEntity(HttpHeaders headers, int status) {
236-
HttpStatus resolvedStatus = HttpStatus.resolve(status);
237-
return resolvedStatus != null
238-
? new ResponseEntity<>(headers, resolvedStatus)
239-
: ResponseEntity.status(status).headers(headers).build();
240-
}
241-
242-
private <T> ResponseEntity<T> createEntity(T body, HttpHeaders headers, int status) {
243-
HttpStatus resolvedStatus = HttpStatus.resolve(status);
244-
return resolvedStatus != null
245-
? new ResponseEntity<>(body, headers, resolvedStatus)
246-
: ResponseEntity.status(status).headers(headers).body(body);
247-
}
248-
249-
250218
private class DefaultHeaders implements Headers {
251219

252220
private HttpHeaders delegate() {

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.springframework.http.HttpRequest;
4040
import org.springframework.http.HttpStatus;
4141
import org.springframework.http.MediaType;
42+
import org.springframework.http.ResponseEntity;
4243
import org.springframework.http.client.reactive.ClientHttpRequest;
4344
import org.springframework.lang.Nullable;
4445
import org.springframework.util.Assert;
@@ -528,6 +529,32 @@ private <T> Mono<T> insertCheckpoint(Mono<T> result, HttpStatus status, HttpRequ
528529
return result.checkpoint(description);
529530
}
530531

532+
@Override
533+
public <T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyClass) {
534+
return this.responseMono.flatMap(response ->
535+
WebClientUtils.toEntity(response, handleBodyMono(response, response.bodyToMono(bodyClass))));
536+
}
537+
538+
@Override
539+
public <T> Mono<ResponseEntity<T>> toEntity(ParameterizedTypeReference<T> bodyTypeReference) {
540+
return this.responseMono.flatMap(response ->
541+
WebClientUtils.toEntity(response,
542+
handleBodyMono(response, response.bodyToMono(bodyTypeReference))));
543+
}
544+
545+
@Override
546+
public <T> Mono<ResponseEntity<List<T>>> toEntityList(Class<T> elementClass) {
547+
return this.responseMono.flatMap(response ->
548+
WebClientUtils.toEntityList(response,
549+
handleBodyFlux(response, response.bodyToFlux(elementClass))));
550+
}
551+
552+
@Override
553+
public <T> Mono<ResponseEntity<List<T>>> toEntityList(ParameterizedTypeReference<T> elementTypeRef) {
554+
return this.responseMono.flatMap(response ->
555+
WebClientUtils.toEntityList(response,
556+
handleBodyFlux(response, response.bodyToFlux(elementTypeRef))));
557+
}
531558

532559
private static class StatusHandler {
533560

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.springframework.http.HttpMethod;
3636
import org.springframework.http.HttpStatus;
3737
import org.springframework.http.MediaType;
38+
import org.springframework.http.ResponseEntity;
3839
import org.springframework.http.client.reactive.ClientHttpConnector;
3940
import org.springframework.http.client.reactive.ClientHttpRequest;
4041
import org.springframework.util.MultiValueMap;
@@ -734,6 +735,48 @@ ResponseSpec onStatus(Predicate<HttpStatus> statusPredicate,
734735
*/
735736
<T> Flux<T> bodyToFlux(ParameterizedTypeReference<T> elementTypeRef);
736737

738+
/**
739+
* Return the response as a delayed {@code ResponseEntity}. By default, if the response has
740+
* status code 4xx or 5xx, the {@code Mono} will contain a {@link WebClientException}. This
741+
* can be overridden with {@link #onStatus(Predicate, Function)}.
742+
* @param bodyClass the expected response body type
743+
* @param <T> response body type
744+
* @return {@code Mono} with the {@code ResponseEntity}
745+
*/
746+
<T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyClass);
747+
748+
/**
749+
* Return the response as a delayed {@code ResponseEntity}. By default, if the response has
750+
* status code 4xx or 5xx, the {@code Mono} will contain a {@link WebClientException}. This
751+
* can be overridden with {@link #onStatus(Predicate, Function)}.
752+
* @param bodyTypeReference a type reference describing the expected response body type
753+
* @param <T> response body type
754+
* @return {@code Mono} with the {@code ResponseEntity}
755+
*/
756+
<T> Mono<ResponseEntity<T>> toEntity(ParameterizedTypeReference<T> bodyTypeReference);
757+
758+
/**
759+
* Return the response as a delayed list of {@code ResponseEntity}s. By default, if the
760+
* response has status code 4xx or 5xx, the {@code Mono} will contain a
761+
* {@link WebClientException}. This can be overridden with
762+
* {@link #onStatus(Predicate, Function)}.
763+
* @param elementClass the expected response body list element class
764+
* @param <T> the type of elements in the list
765+
* @return {@code Mono} with the list of {@code ResponseEntity}s
766+
*/
767+
<T> Mono<ResponseEntity<List<T>>> toEntityList(Class<T> elementClass);
768+
769+
/**
770+
* Return the response as a delayed list of {@code ResponseEntity}s. By default, if the
771+
* response has status code 4xx or 5xx, the {@code Mono} will contain a
772+
* {@link WebClientException}. This can be overridden with
773+
* {@link #onStatus(Predicate, Function)}.
774+
* @param elementTypeRef the expected response body list element reference type
775+
* @param <T> the type of elements in the list
776+
* @return {@code Mono} with the list of {@code ResponseEntity}s
777+
*/
778+
<T> Mono<ResponseEntity<List<T>>> toEntityList(ParameterizedTypeReference<T> elementTypeRef);
779+
737780
}
738781

739782

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.reactive.function.client;
18+
19+
import java.util.List;
20+
21+
import org.reactivestreams.Publisher;
22+
import reactor.core.publisher.Flux;
23+
import reactor.core.publisher.Mono;
24+
25+
import org.springframework.http.HttpHeaders;
26+
import org.springframework.http.HttpStatus;
27+
import org.springframework.http.ResponseEntity;
28+
import org.springframework.lang.Nullable;
29+
30+
/**
31+
* Internal methods shared between {@link DefaultWebClient} and {@link DefaultClientResponse}.
32+
*
33+
* @author Arjen Poutsma
34+
* @since 5.2
35+
*/
36+
abstract class WebClientUtils {
37+
38+
/**
39+
* Create a delayed {@link ResponseEntity} from the given response and body.
40+
*/
41+
public static <T> Mono<ResponseEntity<T>> toEntity(ClientResponse response, Mono<T> bodyMono) {
42+
return Mono.defer(() -> {
43+
HttpHeaders headers = response.headers().asHttpHeaders();
44+
int status = response.rawStatusCode();
45+
return bodyMono
46+
.map(body -> createEntity(body, headers, status))
47+
.switchIfEmpty(Mono.defer(
48+
() -> Mono.just(createEntity(null, headers, status))));
49+
});
50+
}
51+
52+
/**
53+
* Create a delayed {@link ResponseEntity} list from the given response and body.
54+
*/
55+
public static <T> Mono<ResponseEntity<List<T>>> toEntityList(ClientResponse response, Publisher<T> body) {
56+
return Mono.defer(() -> {
57+
HttpHeaders headers = response.headers().asHttpHeaders();
58+
int status = response.rawStatusCode();
59+
return Flux.from(body)
60+
.collectList()
61+
.map(list -> createEntity(list, headers, status));
62+
});
63+
}
64+
65+
private static <T> ResponseEntity<T> createEntity(@Nullable T body, HttpHeaders headers, int status) {
66+
HttpStatus resolvedStatus = HttpStatus.resolve(status);
67+
return resolvedStatus != null
68+
? new ResponseEntity<>(body, headers, resolvedStatus)
69+
: ResponseEntity.status(status).headers(headers).body(body);
70+
}
71+
72+
}

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

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ public void shouldReceiveJsonAsTypeReferenceString() {
219219
}
220220

221221
@Test
222-
public void shouldReceiveJsonAsResponseEntityString() {
222+
public void exchangeShouldReceiveJsonAsResponseEntityString() {
223223
String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";
224224
prepareResponse(response -> response
225225
.setHeader("Content-Type", "application/json").setBody(content));
@@ -246,7 +246,82 @@ public void shouldReceiveJsonAsResponseEntityString() {
246246
}
247247

248248
@Test
249-
public void shouldReceiveJsonAsResponseEntityList() {
249+
public void retrieveShouldReceiveJsonAsResponseEntityString() {
250+
String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";
251+
prepareResponse(response -> response
252+
.setHeader("Content-Type", "application/json").setBody(content));
253+
254+
Mono<ResponseEntity<String>> result = this.webClient.get()
255+
.uri("/json").accept(MediaType.APPLICATION_JSON)
256+
.retrieve()
257+
.toEntity(String.class);
258+
259+
StepVerifier.create(result)
260+
.consumeNextWith(entity -> {
261+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
262+
assertThat(entity.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
263+
assertThat(entity.getHeaders().getContentLength()).isEqualTo(31);
264+
assertThat(entity.getBody()).isEqualTo(content);
265+
})
266+
.expectComplete().verify(Duration.ofSeconds(3));
267+
268+
expectRequestCount(1);
269+
expectRequest(request -> {
270+
assertThat(request.getPath()).isEqualTo("/json");
271+
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
272+
});
273+
}
274+
275+
@Test
276+
public void retrieveEntityWithServerError() {
277+
prepareResponse(response -> response.setResponseCode(500)
278+
.setHeader("Content-Type", "text/plain").setBody("Internal Server error"));
279+
280+
Mono<ResponseEntity<String>> result = this.webClient.get()
281+
.uri("/").accept(MediaType.APPLICATION_JSON)
282+
.retrieve()
283+
.toEntity(String.class);
284+
285+
StepVerifier.create(result)
286+
.expectError(WebClientResponseException.class)
287+
.verify(Duration.ofSeconds(3));
288+
289+
expectRequestCount(1);
290+
expectRequest(request -> {
291+
assertThat(request.getPath()).isEqualTo("/");
292+
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
293+
});
294+
}
295+
296+
@Test
297+
public void retrieveEntityWithServerErrorStatusHandler() {
298+
String content = "Internal Server error";
299+
prepareResponse(response -> response.setResponseCode(500)
300+
.setHeader("Content-Type", "text/plain").setBody(content));
301+
302+
Mono<ResponseEntity<String>> result = this.webClient.get()
303+
.uri("/").accept(MediaType.APPLICATION_JSON)
304+
.retrieve()
305+
.onStatus(HttpStatus::is5xxServerError, response -> Mono.empty())// use normal response
306+
.toEntity(String.class);
307+
308+
StepVerifier.create(result)
309+
.consumeNextWith(entity -> {
310+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
311+
assertThat(entity.getBody()).isEqualTo(content);
312+
})
313+
.expectComplete()
314+
.verify(Duration.ofSeconds(3));
315+
316+
expectRequestCount(1);
317+
expectRequest(request -> {
318+
assertThat(request.getPath()).isEqualTo("/");
319+
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
320+
});
321+
}
322+
323+
@Test
324+
public void exchangeShouldReceiveJsonAsResponseEntityList() {
250325
String content = "[{\"bar\":\"bar1\",\"foo\":\"foo1\"}, {\"bar\":\"bar2\",\"foo\":\"foo2\"}]";
251326
prepareResponse(response -> response
252327
.setHeader("Content-Type", "application/json").setBody(content));
@@ -274,6 +349,57 @@ public void shouldReceiveJsonAsResponseEntityList() {
274349
});
275350
}
276351

352+
@Test
353+
public void retrieveShouldReceiveJsonAsResponseEntityList() {
354+
String content = "[{\"bar\":\"bar1\",\"foo\":\"foo1\"}, {\"bar\":\"bar2\",\"foo\":\"foo2\"}]";
355+
prepareResponse(response -> response
356+
.setHeader("Content-Type", "application/json").setBody(content));
357+
358+
Mono<ResponseEntity<List<Pojo>>> result = this.webClient.get()
359+
.uri("/json").accept(MediaType.APPLICATION_JSON)
360+
.retrieve()
361+
.toEntityList(Pojo.class);
362+
363+
StepVerifier.create(result)
364+
.consumeNextWith(entity -> {
365+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
366+
assertThat(entity.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
367+
assertThat(entity.getHeaders().getContentLength()).isEqualTo(58);
368+
Pojo pojo1 = new Pojo("foo1", "bar1");
369+
Pojo pojo2 = new Pojo("foo2", "bar2");
370+
assertThat(entity.getBody()).isEqualTo(Arrays.asList(pojo1, pojo2));
371+
})
372+
.expectComplete().verify(Duration.ofSeconds(3));
373+
374+
expectRequestCount(1);
375+
expectRequest(request -> {
376+
assertThat(request.getPath()).isEqualTo("/json");
377+
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
378+
});
379+
}
380+
381+
@Test
382+
public void retrieveEntityListWithServerError() {
383+
prepareResponse(response -> response.setResponseCode(500)
384+
.setHeader("Content-Type", "text/plain").setBody("Internal Server error"));
385+
386+
Mono<ResponseEntity<List<String>>> result = this.webClient.get()
387+
.uri("/").accept(MediaType.APPLICATION_JSON)
388+
.retrieve()
389+
.toEntityList(String.class);
390+
391+
StepVerifier.create(result)
392+
.expectError(WebClientResponseException.class)
393+
.verify(Duration.ofSeconds(3));
394+
395+
expectRequestCount(1);
396+
expectRequest(request -> {
397+
assertThat(request.getPath()).isEqualTo("/");
398+
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
399+
});
400+
}
401+
402+
277403
@Test
278404
public void shouldReceiveJsonAsFluxString() {
279405
String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";

0 commit comments

Comments
 (0)