Add status code check to bodyTo[Mono|Flux]

- Add 4xx/5xx status code check to ClientResponse.bodyToMono and bodyToFlux.
- Removed WebClient.retrieveMono and retrieveFlux.

Issue: SPR-14977
This commit is contained in:
Arjen Poutsma
2016-12-06 09:24:29 +01:00
parent 953f7d40cf
commit 079eca9f63
8 changed files with 75 additions and 327 deletions

View File

@@ -32,7 +32,8 @@ import org.springframework.http.codec.BodyExtractor;
/**
* Represents an HTTP response, as returned by the {@link WebClient}.
* Access to headers and body is offered by {@link Headers} and
* {@link #body(BodyExtractor)} respectively.
* {@link #body(BodyExtractor)}, {@link #bodyToMono(Class)}, {@link #bodyToFlux(Class)}
* respectively.
*
* @author Brian Clozel
* @author Arjen Poutsma
@@ -51,7 +52,9 @@ public interface ClientResponse {
Headers headers();
/**
* Extract the body with the given {@code BodyExtractor}.
* 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.
* @param extractor the {@code BodyExtractor} that reads from the response
* @param <T> the type of the body returned
* @return the extracted body
@@ -59,18 +62,22 @@ public interface ClientResponse {
<T> T body(BodyExtractor<T, ? super ClientHttpResponse> extractor);
/**
* Extract the body to a {@code Mono}.
* 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 elementClass the class of element in the {@code Mono}
* @param <T> the element type
* @return the body as a mono
* @return a mono containing the body, or a {@link WebClientException} if the status code is
* 4xx or 5xx
*/
<T> Mono<T> bodyToMono(Class<? extends T> elementClass);
/**
* Extract the body to a {@code Flux}.
* 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 elementClass the class of element in the {@code Flux}
* @param <T> the element type
* @return the body as a flux
* @return a flux containing the body, or a {@link WebClientException} if the status code is
* 4xx or 5xx
*/
<T> Flux<T> bodyToFlux(Class<? extends T> elementClass);

View File

@@ -20,9 +20,11 @@ import java.util.Collections;
import java.util.List;
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;
@@ -77,12 +79,28 @@ class DefaultClientResponse implements ClientResponse {
@Override
public <T> Mono<T> bodyToMono(Class<? extends T> elementClass) {
return body(BodyExtractors.toMono(elementClass));
return bodyToPublisher(BodyExtractors.toMono(elementClass), Mono::error);
}
@Override
public <T> Flux<T> bodyToFlux(Class<? extends T> elementClass) {
return body(BodyExtractors.toFlux(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);
}
}
private class DefaultHeaders implements Headers {

View File

@@ -18,13 +18,9 @@ package org.springframework.web.client.reactive;
import java.util.logging.Level;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.codec.BodyExtractor;
import org.springframework.http.codec.BodyExtractors;
import org.springframework.util.Assert;
/**
@@ -82,33 +78,6 @@ class DefaultWebClientBuilder implements WebClient.Builder {
this.filter = filter;
}
@Override
public <T> Mono<T> retrieveMono(ClientRequest<?> request, Class<? extends T> elementClass) {
Assert.notNull(request, "'request' must not be null");
Assert.notNull(elementClass, "'elementClass' must not be null");
return retrieve(request, BodyExtractors.toMono(elementClass))
.then(m -> m);
}
@Override
public <T> Flux<T> retrieveFlux(ClientRequest<?> request, Class<? extends T> elementClass) {
Assert.notNull(request, "'request' must not be null");
Assert.notNull(elementClass, "'elementClass' must not be null");
return retrieve(request, BodyExtractors.toFlux(elementClass))
.flatMap(flux -> flux);
}
private <T> Mono<T> retrieve(ClientRequest<?> request,
BodyExtractor<T, ? super ClientHttpResponse> extractor) {
ExchangeFilterFunction errorFilter = ExchangeFilterFunctions.clientOrServerError();
return errorFilter.filter(request, this::exchange)
.map(clientResponse -> clientResponse.body(extractor));
}
@Override
public Mono<ClientResponse> exchange(ClientRequest<?> request) {
Assert.notNull(request, "'request' must not be null");

View File

@@ -18,14 +18,10 @@ package org.springframework.web.client.reactive;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
/**
@@ -38,95 +34,6 @@ import org.springframework.util.Assert;
*/
public abstract class ExchangeFilterFunctions {
/**
* Return a filter that will publish a {@link WebClientException} when the
* {@code ClientResponse} has a 4xx status code.
* @return the {@code ExchangeFilterFunction} that publishes a {@code WebClientException} when
* the response has a client error
*/
public static ExchangeFilterFunction clientError() {
return statusError(HttpStatus::is4xxClientError);
}
/**
* Return a filter that will publish a {@link WebClientException} if the
* {@code ClientResponse} has a 5xx status code.
* @return the {@code ExchangeFilterFunction} that publishes a {@code WebClientException} when
* the response has a server error
*/
public static ExchangeFilterFunction serverError() {
return statusError(HttpStatus::is5xxServerError);
}
/**
* Return a filter that will publish a {@link WebClientException} if the
* {@code ClientResponse} has a 4xx or 5xx status code.
* @return the {@code ExchangeFilterFunction} that publishes a {@code WebClientException} when
* the response has a client or server error
*/
public static ExchangeFilterFunction clientOrServerError() {
return clientError().andThen(serverError());
}
private static ExchangeFilterFunction statusError(Predicate<HttpStatus> predicate) {
Function<ClientResponse, Optional<? extends Throwable>> mapper =
clientResponse -> {
HttpStatus status = clientResponse.statusCode();
if (predicate.test(status)) {
return Optional.of(new WebClientException(
"ClientResponse has invalid status code: " + status.value() +
" " + status.getReasonPhrase()));
}
else {
return Optional.empty();
}
};
return errorMapper(mapper);
}
/**
* Return a filter that will publish a {@link WebClientException} if the response satisfies
* the given {@code predicate} function.
* @param predicate the predicate to test the response with
* @return the {@code ExchangeFilterFunction} that publishes a {@code WebClientException} when
* {@code predicate} returns {@code true}
*/
public static ExchangeFilterFunction errorPredicate(Predicate<ClientResponse> predicate) {
Assert.notNull(predicate, "'predicate' must not be null");
Function<ClientResponse, Optional<? extends Throwable>> mapper =
clientResponse -> {
if (predicate.test(clientResponse)) {
return Optional.of(new WebClientException(
"ClientResponse does not satisfy predicate : " + predicate));
}
else {
return Optional.empty();
}
};
return errorMapper(mapper);
}
/**
* Return a filter that maps the response to a potential error. Exceptions returned by
* {@code mapper} will be published as signal in the {@code Mono<ClientResponse>} return value.
* @param mapper the function that maps from response to optional error
* @return the {@code ExchangeFilterFunction} that propagates the errors provided by
* {@code mapper}
*/
public static ExchangeFilterFunction errorMapper(Function<ClientResponse,
Optional<? extends Throwable>> mapper) {
Assert.notNull(mapper, "'mapper' must not be null");
return ExchangeFilterFunction.ofResponseProcessor(
clientResponse -> {
Optional<? extends Throwable> error = mapper.apply(clientResponse);
return error.isPresent() ? Mono.error(error.get()) : Mono.just(clientResponse);
});
}
/**
* Return a filter that adds an Authorization header for HTTP Basic Authentication.
* @param username the username to use

View File

@@ -16,7 +16,6 @@
package org.springframework.web.client.reactive;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.http.client.reactive.ClientHttpConnector;
@@ -24,9 +23,7 @@ import org.springframework.util.Assert;
/**
* Reactive Web client supporting the HTTP/1.1 protocol. Main entry point is through the
* {@link #exchange(ClientRequest)} method, or through the
* {@link #retrieveMono(ClientRequest, Class)} and {@link #retrieveFlux(ClientRequest, Class)}
* convenience methods.
* {@link #exchange(ClientRequest)} method.
*
* <p>For example:
* <pre class="code">
@@ -35,54 +32,24 @@ import org.springframework.util.Assert;
*
* Mono&lt;String&gt; result = client
* .exchange(request)
* .then(response -> response.body(BodyExtractors.toMono(String.class)));
* </pre>
* <p>or, by using {@link #retrieveMono(ClientRequest, Class)}:
* <pre class="code">
* Mono&lt;String&gt; result = client.retrieveMono(request, String.class);
* .then(response -> response.bodyToMono(String.class));
* </pre>
*
* @author Brian Clozel
* @author Arjen Poutsma
* @since 5.0
*/
public interface WebClient {
public interface WebClient extends ExchangeFunction {
/**
* Exchange the given request for a response mono. Invoking this method performs the actual
* HTTP request/response exchange.
* <p>Note that this method will <strong>not</strong> publish an exception if the response
* has a 4xx or 5xx status code; as opposed to {@link #retrieveMono(ClientRequest, Class)} and
* {@link #retrieveFlux(ClientRequest, Class)}.
* @param request the request to exchange
* @return the response, wrapped in a {@code Mono}
*/
@Override
Mono<ClientResponse> exchange(ClientRequest<?> request);
/**
* Retrieve the body of the response as a {@code Mono}. A 4xx or 5xx status
* code in the response will result in a {@link WebClientException} published in the returned
* {@code Mono}.
* @param request the request to exchange
* @param elementClass the class of element in the {@code Mono}
* @param <T> the element type
* @return the response body as a mono
* @see ExchangeFilterFunctions#clientOrServerError()
*/
<T> Mono<T> retrieveMono(ClientRequest<?> request, Class<? extends T> elementClass);
/**
* Retrieve the body of the response as a {@code Flux}. A 4xx or 5xx status
* code in the response will result in a {@link WebClientException} published in the returned
* {@code Flux}.
* @param request the request to exchange
* @param elementClass the class of element in the {@code Flux}
* @param <T> the element type
* @return the response body as a flux
* @see ExchangeFilterFunctions#clientOrServerError()
*/
<T> Flux<T> retrieveFlux(ClientRequest<?> request, Class<? extends T> elementClass);
/**
* Create a new instance of {@code WebClient} with the given connector. This method uses