Consistent handling of 4xx/5xx status codes in WebClient
This commit changes the handling of 4xx/5xx status codes in the WebClient to the following simple rule: if there is no way for the user to get the response status code, then a WebClientException is returned. If there is a way to get to the status code, then we do not return an exception. Issue: SPR-15486
This commit is contained in:
parent
0e7d6fc4d1
commit
4a8c99c9ce
|
@ -59,9 +59,7 @@ public interface ClientResponse {
|
|||
MultiValueMap<String, ResponseCookie> cookies();
|
||||
|
||||
/**
|
||||
* Extract the body with the given {@code BodyExtractor}. Unlike {@link #bodyToMono(Class)} and
|
||||
* {@link #bodyToFlux(Class)}; this method does not check for a 4xx or 5xx status code before
|
||||
* extracting the body.
|
||||
* Extract the body with the given {@code BodyExtractor}.
|
||||
* @param extractor the {@code BodyExtractor} that reads from the response
|
||||
* @param <T> the type of the body returned
|
||||
* @return the extracted body
|
||||
|
@ -69,22 +67,18 @@ public interface ClientResponse {
|
|||
<T> T body(BodyExtractor<T, ? super ClientHttpResponse> extractor);
|
||||
|
||||
/**
|
||||
* Extract the body to a {@code Mono}. If the response has status code 4xx or 5xx, the
|
||||
* {@code Mono} will contain a {@link WebClientException}.
|
||||
* Extract the body to a {@code Mono}.
|
||||
* @param elementClass the class of element in the {@code Mono}
|
||||
* @param <T> the element type
|
||||
* @return a mono containing the body, or a {@link WebClientException} if the status code is
|
||||
* 4xx or 5xx
|
||||
* @return a mono containing the body of the given type {@code T}
|
||||
*/
|
||||
<T> Mono<T> bodyToMono(Class<? extends T> elementClass);
|
||||
|
||||
/**
|
||||
* Extract the body to a {@code Flux}. If the response has status code 4xx or 5xx, the
|
||||
* {@code Flux} will contain a {@link WebClientException}.
|
||||
* Extract the body to a {@code Flux}.
|
||||
* @param elementClass the class of element in the {@code Flux}
|
||||
* @param <T> the element type
|
||||
* @return a flux containing the body, or a {@link WebClientException} if the status code is
|
||||
* 4xx or 5xx
|
||||
* @return a flux containing the body of the given type {@code T}
|
||||
*/
|
||||
<T> Flux<T> bodyToFlux(Class<? extends T> elementClass);
|
||||
|
||||
|
|
|
@ -21,11 +21,9 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
|
@ -99,28 +97,12 @@ class DefaultClientResponse implements ClientResponse {
|
|||
|
||||
@Override
|
||||
public <T> Mono<T> bodyToMono(Class<? extends T> elementClass) {
|
||||
return bodyToPublisher(BodyExtractors.toMono(elementClass), Mono::error);
|
||||
return body(BodyExtractors.toMono(elementClass));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> bodyToFlux(Class<? extends T> elementClass) {
|
||||
return bodyToPublisher(BodyExtractors.toFlux(elementClass), Flux::error);
|
||||
}
|
||||
|
||||
private <T extends Publisher<?>> T bodyToPublisher(
|
||||
BodyExtractor<T, ? super ClientHttpResponse> extractor,
|
||||
Function<WebClientException, T> errorFunction) {
|
||||
|
||||
HttpStatus status = statusCode();
|
||||
if (status.is4xxClientError() || status.is5xxServerError()) {
|
||||
WebClientException ex = new WebClientException(
|
||||
"ClientResponse has erroneous status code: " + status.value() +
|
||||
" " + status.getReasonPhrase());
|
||||
return errorFunction.apply(ex);
|
||||
}
|
||||
else {
|
||||
return body(extractor);
|
||||
}
|
||||
return body(BodyExtractors.toFlux(elementClass));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -32,13 +32,17 @@ import reactor.core.publisher.Mono;
|
|||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.BodyExtractor;
|
||||
import org.springframework.web.reactive.function.BodyExtractors;
|
||||
import org.springframework.web.reactive.function.BodyInserter;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
|
@ -350,14 +354,35 @@ class DefaultWebClient implements WebClient {
|
|||
|
||||
@Override
|
||||
public <T> Mono<T> bodyToMono(Class<T> bodyType) {
|
||||
return this.responseMono.flatMap(clientResponse -> clientResponse.bodyToMono(bodyType));
|
||||
return this.responseMono.flatMap(
|
||||
response -> bodyToPublisher(response, BodyExtractors.toMono(bodyType),
|
||||
Mono::error));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> bodyToFlux(Class<T> elementType) {
|
||||
return this.responseMono.flatMapMany(clientResponse -> clientResponse.bodyToFlux(elementType));
|
||||
return this.responseMono.flatMapMany(
|
||||
response -> bodyToPublisher(response, BodyExtractors.toFlux(elementType),
|
||||
Flux::error));
|
||||
}
|
||||
|
||||
private <T extends Publisher<?>> T bodyToPublisher(ClientResponse response,
|
||||
BodyExtractor<T, ? super ClientHttpResponse> extractor,
|
||||
Function<WebClientException, T> errorFunction) {
|
||||
|
||||
HttpStatus status = response.statusCode();
|
||||
if (status.is4xxClientError() || status.is5xxServerError()) {
|
||||
WebClientException ex = new WebClientException(
|
||||
"ClientResponse has erroneous status code: " + status.value() +
|
||||
" " + status.getReasonPhrase());
|
||||
return errorFunction.apply(ex);
|
||||
}
|
||||
else {
|
||||
return response.body(extractor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType) {
|
||||
return this.responseMono.flatMap(response ->
|
||||
|
|
|
@ -477,42 +477,46 @@ public interface WebClient {
|
|||
interface ResponseSpec {
|
||||
|
||||
/**
|
||||
* Extract the response body to an Object of type {@code <T>} by
|
||||
* invoking {@link ClientResponse#bodyToMono(Class)}.
|
||||
* Extract the body to a {@code Mono}. If the response has status code 4xx or 5xx, the
|
||||
* {@code Mono} will contain a {@link WebClientException}.
|
||||
*
|
||||
* @param bodyType the expected response body type
|
||||
* @param <T> response body type
|
||||
* @return {@code Mono} with the result
|
||||
* @return a mono containing the body, or a {@link WebClientException} if the status code is
|
||||
* 4xx or 5xx
|
||||
*/
|
||||
<T> Mono<T> bodyToMono(Class<T> bodyType);
|
||||
|
||||
/**
|
||||
* Extract the response body to a stream of Objects of type {@code <T>}
|
||||
* by invoking {@link ClientResponse#bodyToFlux(Class)}.
|
||||
* Extract the body to a {@code Flux}. If the response has status code 4xx or 5xx, the
|
||||
* {@code Flux} will contain a {@link WebClientException}.
|
||||
*
|
||||
* @param elementType the type of element in the response
|
||||
* @param <T> the type of elements in the response
|
||||
* @return the body of the response
|
||||
* @return a flux containing the body, or a {@link WebClientException} if the status code is
|
||||
* 4xx or 5xx
|
||||
*/
|
||||
<T> Flux<T> bodyToFlux(Class<T> elementType);
|
||||
|
||||
/**
|
||||
* A variant of {@link #bodyToMono(Class)} that also provides access to
|
||||
* the response status and headers.
|
||||
* Returns the response as a delayed {@code ResponseEntity}. Unlike
|
||||
* {@link #bodyToMono(Class)} and {@link #bodyToFlux(Class)}, this method does not check
|
||||
* for a 4xx or 5xx status code before extracting the body.
|
||||
*
|
||||
* @param bodyType the expected response body type
|
||||
* @param <T> response body type
|
||||
* @return {@code Mono} with the result
|
||||
* @return {@code Mono} with the {@code ResponseEntity}
|
||||
*/
|
||||
<T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType);
|
||||
|
||||
/**
|
||||
* A variant of {@link #bodyToFlux(Class)} collected via
|
||||
* {@link Flux#collectList()} and wrapped in {@code ResponseEntity}.
|
||||
* Returns the response as a delayed list of {@code ResponseEntity}s. Unlike
|
||||
* {@link #bodyToMono(Class)} and {@link #bodyToFlux(Class)}, this method does not check
|
||||
* for a 4xx or 5xx status code before extracting the body.
|
||||
*
|
||||
* @param elementType the expected response body list element type
|
||||
* @param <T> the type of elements in the list
|
||||
* @return {@code Mono} with the result
|
||||
* @return {@code Mono} with the list of {@code ResponseEntity}s
|
||||
*/
|
||||
<T> Mono<ResponseEntity<List<T>>> toEntityList(Class<T> elementType);
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@ import org.junit.Before;
|
|||
import org.junit.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
|
@ -48,7 +47,7 @@ import org.springframework.util.MultiValueMap;
|
|||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.web.reactive.function.BodyExtractors.*;
|
||||
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
|
@ -151,24 +150,6 @@ public class DefaultClientResponseTests {
|
|||
assertEquals("foo", resultMono.block());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bodyToMonoError() throws Exception {
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
|
||||
when(mockResponse.getHeaders()).thenReturn(httpHeaders);
|
||||
when(mockResponse.getStatusCode()).thenReturn(HttpStatus.NOT_FOUND);
|
||||
|
||||
Set<HttpMessageReader<?>> messageReaders = Collections
|
||||
.singleton(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true)));
|
||||
when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders::stream);
|
||||
|
||||
Mono<String> resultMono = defaultClientResponse.bodyToMono(String.class);
|
||||
|
||||
StepVerifier.create(resultMono)
|
||||
.expectError(WebClientException.class)
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bodyToFlux() throws Exception {
|
||||
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
|
||||
|
@ -191,21 +172,4 @@ public class DefaultClientResponseTests {
|
|||
assertEquals(Collections.singletonList("foo"), result.block());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bodyToFluxError() throws Exception {
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
|
||||
when(mockResponse.getHeaders()).thenReturn(httpHeaders);
|
||||
when(mockResponse.getStatusCode()).thenReturn(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
|
||||
Set<HttpMessageReader<?>> messageReaders = Collections
|
||||
.singleton(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true)));
|
||||
when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders::stream);
|
||||
|
||||
Flux<String> resultFlux = defaultClientResponse.bodyToFlux(String.class);
|
||||
StepVerifier.create(resultFlux)
|
||||
.expectError(WebClientException.class)
|
||||
.verify();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -334,7 +334,7 @@ public class WebClientIntegrationTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void notFound() throws Exception {
|
||||
public void exchangeNotFound() throws Exception {
|
||||
this.server.enqueue(new MockResponse().setResponseCode(404)
|
||||
.setHeader("Content-Type", "text/plain").setBody("Not Found"));
|
||||
|
||||
|
@ -351,6 +351,47 @@ public class WebClientIntegrationTests {
|
|||
Assert.assertEquals("/greeting?name=Spring", recordedRequest.getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retrieveBodyToMonoNotFound() throws Exception {
|
||||
this.server.enqueue(new MockResponse().setResponseCode(404)
|
||||
.setHeader("Content-Type", "text/plain").setBody("Not Found"));
|
||||
|
||||
Mono<String> result = this.webClient.get()
|
||||
.uri("/greeting?name=Spring")
|
||||
.retrieve()
|
||||
.bodyToMono(String.class);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectError(WebClientException.class)
|
||||
.verify(Duration.ofSeconds(3));
|
||||
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
Assert.assertEquals(1, server.getRequestCount());
|
||||
Assert.assertEquals("*/*", recordedRequest.getHeader(HttpHeaders.ACCEPT));
|
||||
Assert.assertEquals("/greeting?name=Spring", recordedRequest.getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retrieveToEntityNotFound() throws Exception {
|
||||
this.server.enqueue(new MockResponse().setResponseCode(404)
|
||||
.setHeader("Content-Type", "text/plain").setBody("Not Found"));
|
||||
|
||||
Mono<ResponseEntity<String>> result = this.webClient.get()
|
||||
.uri("/greeting?name=Spring")
|
||||
.retrieve()
|
||||
.toEntity(String.class);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(response -> assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()))
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3));
|
||||
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
Assert.assertEquals(1, server.getRequestCount());
|
||||
Assert.assertEquals("*/*", recordedRequest.getHeader(HttpHeaders.ACCEPT));
|
||||
Assert.assertEquals("/greeting?name=Spring", recordedRequest.getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildFilter() throws Exception {
|
||||
this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
|
||||
|
|
Loading…
Reference in New Issue