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 ae2c0518460b..99921bbe073e 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 @@ -60,9 +60,22 @@ public interface ClientResponse { /** * Return the status code of this response. + * @return the status as an HttpStatus enum value + * @throws IllegalArgumentException in case of an unknown HTTP status code + * @see HttpStatus#valueOf(int) */ HttpStatus statusCode(); + /** + * Return the status code (potentially non-standard and not resolvable + * through the {@link HttpStatus} enum) as an integer. + * @return the status as an integer + * @since 5.0.7 + * @see #statusCode() + * @see HttpStatus#resolve(int) + */ + int rawStatusCode(); + /** * Return the headers of this response. */ @@ -172,6 +185,17 @@ static Builder create(HttpStatus statusCode) { return create(statusCode, ExchangeStrategies.withDefaults()); } + /** + * Create a response builder with the given status code and using default strategies for + * reading the body. + * @param statusCode the status code + * @return the created builder + * @since 5.0.7 + */ + static Builder create(int statusCode) { + return create(statusCode, ExchangeStrategies.withDefaults()); + } + /** * Create a response builder with the given status code and strategies for reading the body. * @param statusCode the status code @@ -182,6 +206,17 @@ static Builder create(HttpStatus statusCode, ExchangeStrategies strategies) { return new DefaultClientResponseBuilder(strategies).statusCode(statusCode); } + /** + * Create a response builder with the given status code and strategies for reading the body. + * @param statusCode the status code + * @param strategies the strategies + * @return the created builder + * @since 5.0.7 + */ + static Builder create(int statusCode, ExchangeStrategies strategies) { + return new DefaultClientResponseBuilder(strategies).statusCode(statusCode); + } + /** * Create a response builder with the given status code and message body readers. * @param statusCode the status code @@ -202,6 +237,27 @@ public List> messageWriters() { }); } + /** + * Create a response builder with the given status code and message body readers. + * @param statusCode the status code + * @param messageReaders the message readers + * @return the created builder + * @since 5.0.7 + */ + static Builder create(int statusCode, List> messageReaders) { + return create(statusCode, new ExchangeStrategies() { + @Override + public List> messageReaders() { + return messageReaders; + } + @Override + public List> messageWriters() { + // not used in the response + return Collections.emptyList(); + } + }); + } + /** * Represents the headers of the HTTP response. @@ -247,6 +303,14 @@ interface Builder { */ Builder statusCode(HttpStatus statusCode); + /** + * Set the status code of the response. + * @param statusCode the new status code. + * @return this builder + * @since 5.0.7 + */ + Builder statusCode(int statusCode); + /** * Add the given header value(s) under the given name. * @param headerName the header name 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 64647ccdb1b7..b9f2cb2da712 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 @@ -72,6 +72,11 @@ public HttpStatus statusCode() { return this.response.getStatusCode(); } + @Override + public int rawStatusCode() { + return this.response.getRawStatusCode(); + } + @Override public Headers headers() { return this.headers; @@ -173,11 +178,11 @@ public Mono> toEntity(ParameterizedTypeReference typeRe private Mono> toEntityInternal(Mono bodyMono) { HttpHeaders headers = headers().asHttpHeaders(); - HttpStatus statusCode = statusCode(); + int status = rawStatusCode(); return bodyMono - .map(body -> new ResponseEntity<>(body, headers, statusCode)) + .map(body -> createEntity(body, headers, status)) .switchIfEmpty(Mono.defer( - () -> Mono.just(new ResponseEntity<>(headers, statusCode)))); + () -> Mono.just(createEntity(headers, status)))); } @Override @@ -192,10 +197,24 @@ public Mono>> toEntityList(ParameterizedTypeReference private Mono>> toEntityListInternal(Flux bodyFlux) { HttpHeaders headers = headers().asHttpHeaders(); - HttpStatus statusCode = statusCode(); + int status = rawStatusCode(); return bodyFlux .collectList() - .map(body -> new ResponseEntity<>(body, headers, statusCode)); + .map(body -> createEntity(body, headers, status)); + } + + private ResponseEntity createEntity(HttpHeaders headers, int status) { + HttpStatus resolvedStatus = HttpStatus.resolve(status); + return resolvedStatus != null + ? new ResponseEntity<>(headers, resolvedStatus) + : ResponseEntity.status(status).headers(headers).build(); + } + + private ResponseEntity createEntity(T body, HttpHeaders headers, int status) { + HttpStatus resolvedStatus = HttpStatus.resolve(status); + return resolvedStatus != null + ? new ResponseEntity<>(body, headers, resolvedStatus) + : ResponseEntity.status(status).headers(headers).body(body); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java index 1320c20d32d5..e0d7499b5c9d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java @@ -44,7 +44,7 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder { private ExchangeStrategies strategies; - private HttpStatus statusCode = HttpStatus.OK; + private int statusCode = HttpStatus.OK.value(); private final HttpHeaders headers = new HttpHeaders(); @@ -61,7 +61,7 @@ public DefaultClientResponseBuilder(ExchangeStrategies strategies) { public DefaultClientResponseBuilder(ClientResponse other) { Assert.notNull(other, "ClientResponse must not be null"); this.strategies = other.strategies(); - statusCode(other.statusCode()); + statusCode(other.rawStatusCode()); headers(headers -> headers.addAll(other.headers().asHttpHeaders())); cookies(cookies -> cookies.addAll(other.cookies())); } @@ -70,6 +70,12 @@ public DefaultClientResponseBuilder(ClientResponse other) { @Override public DefaultClientResponseBuilder statusCode(HttpStatus statusCode) { Assert.notNull(statusCode, "HttpStatus must not be null"); + this.statusCode = statusCode.value(); + return this; + } + + @Override + public DefaultClientResponseBuilder statusCode(int statusCode) { this.statusCode = statusCode; return this; } @@ -137,7 +143,7 @@ public ClientResponse build() { private static class BuiltClientHttpResponse implements ClientHttpResponse { - private final HttpStatus statusCode; + private final int statusCode; private final HttpHeaders headers; @@ -145,7 +151,7 @@ private static class BuiltClientHttpResponse implements ClientHttpResponse { private final Flux body; - public BuiltClientHttpResponse(HttpStatus statusCode, HttpHeaders headers, + public BuiltClientHttpResponse(int statusCode, HttpHeaders headers, MultiValueMap cookies, Flux body) { this.statusCode = statusCode; @@ -156,12 +162,12 @@ public BuiltClientHttpResponse(HttpStatus statusCode, HttpHeaders headers, @Override public HttpStatus getStatusCode() { - return this.statusCode; + return HttpStatus.valueOf(this.statusCode); } @Override public int getRawStatusCode() { - return this.statusCode.value(); + return this.statusCode; } @Override 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 3cac85cfc6bd..3602696a6188 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 @@ -59,6 +59,7 @@ * Default implementation of {@link WebClient}. * * @author Rossen Stoyanchev + * @author Denys Ivano * @since 5.0 */ class DefaultWebClient implements WebClient { @@ -369,11 +370,12 @@ public ResponseSpec retrieve() { private static class DefaultResponseSpec implements ResponseSpec { private static final StatusHandler DEFAULT_STATUS_HANDLER = - new StatusHandler(HttpStatus::isError, DefaultResponseSpec::createResponseException); + new StatusHandler(StatusCodePredicates.isError(), + DefaultResponseSpec::createResponseException); private final Mono responseMono; - private List statusHandlers = new ArrayList<>(1); + private final List statusHandlers = new ArrayList<>(1); DefaultResponseSpec(Mono responseMono) { this.responseMono = responseMono; @@ -384,12 +386,26 @@ private static class DefaultResponseSpec implements ResponseSpec { public ResponseSpec onStatus(Predicate statusPredicate, Function> exceptionFunction) { + removeDefaultStatusHandler(); + this.statusHandlers.add(new HttpStatusHandler(statusPredicate, exceptionFunction)); + + return this; + } + + @Override + public ResponseSpec onStatusCode(Predicate statusCodePredicate, + Function> exceptionFunction) { + + removeDefaultStatusHandler(); + this.statusHandlers.add(new StatusHandler(statusCodePredicate, exceptionFunction)); + + return this; + } + + private void removeDefaultStatusHandler() { if (this.statusHandlers.size() == 1 && this.statusHandlers.get(0) == DEFAULT_STATUS_HANDLER) { this.statusHandlers.clear(); } - this.statusHandlers.add(new StatusHandler(statusPredicate, exceptionFunction)); - - return this; } @Override @@ -435,7 +451,7 @@ private > T bodyToPublisher(ClientResponse response, T bodyPublisher, Function, T> errorFunction) { return this.statusHandlers.stream() - .filter(statusHandler -> statusHandler.test(response.statusCode())) + .filter(statusHandler -> statusHandler.test(response.rawStatusCode())) .findFirst() .map(statusHandler -> statusHandler.apply(response)) .map(errorFunction::apply) @@ -453,14 +469,16 @@ private static Mono createResponseException(ClientRe }) .defaultIfEmpty(new byte[0]) .map(bodyBytes -> { - String msg = String.format("ClientResponse has erroneous status code: %d %s", response.statusCode().value(), - response.statusCode().getReasonPhrase()); + int status = response.rawStatusCode(); + HttpStatus resolvedStatus = HttpStatus.resolve(status); + String msg = "ClientResponse has erroneous status code: " + status + + (resolvedStatus != null ? " " + resolvedStatus.getReasonPhrase() : ""); Charset charset = response.headers().contentType() .map(MimeType::getCharset) .orElse(StandardCharsets.ISO_8859_1); return new WebClientResponseException(msg, - response.statusCode().value(), - response.statusCode().getReasonPhrase(), + status, + resolvedStatus != null ? resolvedStatus.getReasonPhrase() : null, response.headers().asHttpHeaders(), bodyBytes, charset @@ -471,11 +489,11 @@ private static Mono createResponseException(ClientRe private static class StatusHandler { - private final Predicate predicate; + private final Predicate predicate; private final Function> exceptionFunction; - public StatusHandler(Predicate predicate, + public StatusHandler(Predicate predicate, Function> exceptionFunction) { Assert.notNull(predicate, "Predicate must not be null"); @@ -484,7 +502,7 @@ public StatusHandler(Predicate predicate, this.exceptionFunction = exceptionFunction; } - public boolean test(HttpStatus status) { + public boolean test(int status) { return this.predicate.test(status); } @@ -492,6 +510,22 @@ public Mono apply(ClientResponse response) { return this.exceptionFunction.apply(response); } } + + private static class HttpStatusHandler extends StatusHandler { + + public HttpStatusHandler(Predicate predicate, + Function> exceptionFunction) { + + super(statusCodePredicate(predicate), exceptionFunction); + } + + private static Predicate statusCodePredicate(Predicate predicate) { + return status -> { + HttpStatus resolvedStatus = HttpStatus.resolve(status); + return resolvedStatus != null && predicate.test(resolvedStatus); + }; + } + } } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java index 04dcf26a69db..9a3845eb0140 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java @@ -37,6 +37,7 @@ * * @author Rob Winch * @author Arjen Poutsma + * @author Denys Ivano * @since 5.0 */ public abstract class ExchangeFilterFunctions { @@ -124,8 +125,10 @@ private static void checkIllegalCharacters(String username, String password) { /** * Return a filter that returns a given {@link Throwable} as response if the given * {@link HttpStatus} predicate matches. - * @param statusPredicate the predicate that should match the - * {@linkplain ClientResponse#statusCode() response status} + *

Note: when the response contains status code that can't be + * resolved through {@link HttpStatus} enum, the specified predicate and + * exception function will not be applied. + * @param statusPredicate the predicate that should match the response status * @param exceptionFunction the function that returns the exception * @return the {@link ExchangeFilterFunction} that returns the given exception if the predicate * matches @@ -138,7 +141,38 @@ public static ExchangeFilterFunction statusError(Predicate statusPre return ExchangeFilterFunction.ofResponseProcessor( clientResponse -> { - if (statusPredicate.test(clientResponse.statusCode())) { + int status = clientResponse.rawStatusCode(); + HttpStatus resolvedStatus = HttpStatus.resolve(status); + if (resolvedStatus != null && statusPredicate.test(resolvedStatus)) { + return Mono.error(exceptionFunction.apply(clientResponse)); + } + else { + return Mono.just(clientResponse); + } + } + ); + } + + /** + * Return a filter that returns a given {@link Throwable} as response if the given + * response status predicate matches. + * @param statusCodePredicate the predicate that should match the + * {@linkplain ClientResponse#rawStatusCode() response status} + * @param exceptionFunction the function that returns the exception + * @return the {@link ExchangeFilterFunction} that returns the given exception if the predicate + * matches + * @since 5.0.7 + * @see StatusCodePredicates + */ + public static ExchangeFilterFunction statusCodeError(Predicate statusCodePredicate, + Function exceptionFunction) { + + Assert.notNull(statusCodePredicate, "Predicate must not be null"); + Assert.notNull(exceptionFunction, "Function must not be null"); + + return ExchangeFilterFunction.ofResponseProcessor( + clientResponse -> { + if (statusCodePredicate.test(clientResponse.rawStatusCode())) { return Mono.error(exceptionFunction.apply(clientResponse)); } else { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/StatusCodePredicates.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/StatusCodePredicates.java new file mode 100644 index 000000000000..d22596fdaa0a --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/StatusCodePredicates.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.function.client; + +import java.util.function.Predicate; + +import org.springframework.util.Assert; + +/** + * Provides various methods for creating status code predicates. + * + * @author Denys Ivano + * @since 5.0.7 + */ +public abstract class StatusCodePredicates { + + /** + * Return a status code predicate that tests whether the status code is in the + * HTTP 1xx Informational series. + * @return a predicate that matches on 1xx status codes + */ + public static Predicate is1xxInformational() { + return between(100, 199); + } + + /** + * Return a status code predicate that tests whether the status code is in the + * HTTP 2xx Successful series. + * @return a predicate that matches on 2xx status codes + */ + public static Predicate is2xxSuccessful() { + return between(200, 299); + } + + /** + * Return a status code predicate that tests whether the status code is in the + * HTTP 3xx Redirection series. + * @return a predicate that matches on 3xx status codes + */ + public static Predicate is3xxRedirection() { + return between(300, 399); + } + + /** + * Return a status code predicate that tests whether the status code is in the + * HTTP 4xx Client Error series. + * @return a predicate that matches on 4xx status codes + */ + public static Predicate is4xxClientError() { + return between(400, 499); + } + + /** + * Return a status code predicate that tests whether the status code is in the + * HTTP 5xx Server Error series. + * @return a predicate that matches on 5xx status codes + */ + public static Predicate is5xxServerError() { + return between(500, 599); + } + + /** + * Return a status code predicate that tests whether the status code is in the + * HTTP 4xx Client Error or 5xx Server Error series. + * @return a predicate that matches on 4xx and 5xx status codes + */ + public static Predicate isError() { + return between(400, 599); + } + + /** + * Return a status code predicate that tests whether the status code is between + * the specified values. + * @param from value from (inclusive) + * @param to value to (inclusive) + * @return the status code predicate + * @throws IllegalArgumentException if 'from' value is greater than 'to' + */ + public static Predicate between(int from, int to) { + Assert.isTrue(from <= to, "'from' value must be <= than 'to'"); + return status -> status >= from && status <= to; + } + + /** + * Return a status code predicate that tests whether the status code is equal + * to the specified status. + * @param statusCode the status code to test against + * @return the status code predicate + */ + public static Predicate is(int statusCode) { + return status -> status == statusCode; + } + + /** + * Return a status code predicate that tests whether the status code is not equal + * to the specified status. + * @param statusCode the status code to test against + * @return the status code predicate + */ + public static Predicate not(int statusCode) { + return status -> status != statusCode; + } + + /** + * Return a status code predicate that tests whether the status code is among + * the specified statuses. + * @param statusCodes the status codes to test against + * @return the status code predicate + */ + public static Predicate anyOf(int ... statusCodes) { + Assert.notNull(statusCodes, "Status codes must not be null"); + return status -> { + for (int s : statusCodes) { + if (s == status) { + return true; + } + } + return false; + }; + } + + /** + * Return a status code predicate that tests whether the status code is not among + * the specified statuses. + * @param statusCodes the status codes to test against + * @return the status code predicate + */ + public static Predicate noneOf(int ... statusCodes) { + return anyOf(statusCodes).negate(); + } + +} 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 20581377ffc7..507fc6a7daba 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 @@ -592,21 +592,44 @@ interface ResponseSpec { /** * Register a custom error function that gets invoked when the given {@link HttpStatus} * predicate applies. The exception returned from the function will be returned from - * {@link #bodyToMono(Class)} and {@link #bodyToFlux(Class)}. - *

By default, an error handler is register that throws a + * {@link #bodyToMono(Class)}, {@link #bodyToMono(ParameterizedTypeReference)}, + * {@link #bodyToFlux(Class)} and {@link #bodyToFlux(ParameterizedTypeReference)}. + *

By default, an error handler is registered that throws a * {@link WebClientResponseException} when the response status code is 4xx or 5xx. + *

Note: when the response contains status code that can't be resolved through + * {@link HttpStatus} enum, the specified predicate and exception function will not be + * applied. * @param statusPredicate a predicate that indicates whether {@code exceptionFunction} * applies * @param exceptionFunction the function that returns the exception * @return this builder + * @see #onStatusCode(Predicate, Function) */ ResponseSpec onStatus(Predicate statusPredicate, Function> exceptionFunction); + /** + * Register a custom error function that gets invoked when the given status code + * predicate applies. The exception returned from the function will be returned from + * {@link #bodyToMono(Class)}, {@link #bodyToMono(ParameterizedTypeReference)}, + * {@link #bodyToFlux(Class)} and {@link #bodyToFlux(ParameterizedTypeReference)}. + *

By default, an error handler is registered that throws a + * {@link WebClientResponseException} when the response status code is 4xx or 5xx. + * @param statusCodePredicate a predicate that indicates whether {@code exceptionFunction} + * applies + * @param exceptionFunction the function that returns the exception + * @return this builder + * @since 5.0.7 + * @see StatusCodePredicates + */ + ResponseSpec onStatusCode(Predicate statusCodePredicate, + Function> exceptionFunction); + /** * Extract the body to a {@code Mono}. 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)}. + * 5xx, the {@code Mono} will contain a {@link WebClientResponseException}. This can be + * overridden with {@link #onStatus(Predicate, Function)} and + * {@link #onStatusCode(Predicate, Function)}. * @param bodyType the expected response body type * @param response body type * @return a mono containing the body, or a {@link WebClientResponseException} if the @@ -616,8 +639,9 @@ ResponseSpec onStatus(Predicate statusPredicate, /** * Extract the body to a {@code Mono}. 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)}. + * 5xx, the {@code Mono} will contain a {@link WebClientResponseException}. This can be + * overridden with {@link #onStatus(Predicate, Function)} and + * {@link #onStatusCode(Predicate, Function)} * @param typeReference a type reference describing the expected response body type * @param response body type * @return a mono containing the body, or a {@link WebClientResponseException} if the @@ -627,9 +651,10 @@ ResponseSpec onStatus(Predicate statusPredicate, /** * Extract the body to a {@code Flux}. By default, if the response has status code 4xx or - * 5xx, the {@code Flux} will contain a {@link WebClientException}. This can be overridden - * with {@link #onStatus(Predicate, Function)}. - * @param elementType the type of element in the response + * 5xx, the {@code Flux} will contain a {@link WebClientResponseException}. This can be + * overridden with {@link #onStatus(Predicate, Function)} and + * {@link #onStatusCode(Predicate, Function)} + * @param elementType the type of elements in the response * @param the type of elements in the response * @return a flux containing the body, or a {@link WebClientResponseException} if the * status code is 4xx or 5xx @@ -638,9 +663,10 @@ ResponseSpec onStatus(Predicate statusPredicate, /** * Extract the body to a {@code Flux}. By default, if the response has status code 4xx or - * 5xx, the {@code Flux} will contain a {@link WebClientException}. This can be overridden - * with {@link #onStatus(Predicate, Function)}. - * @param typeReference a type reference describing the expected response body type + * 5xx, the {@code Flux} will contain a {@link WebClientResponseException}. This can be + * overridden with {@link #onStatus(Predicate, Function)} and + * {@link #onStatusCode(Predicate, Function)} + * @param typeReference a type reference describing the type of elements in the response * @param the type of elements in the response * @return a flux containing the body, or a {@link WebClientResponseException} if the * status code is 4xx or 5xx diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java index 44d11558d5cd..1b178168ca36 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ public class WebClientResponseException extends WebClientException { private final int statusCode; + @Nullable private final String statusText; private final byte[] responseBody; @@ -47,13 +48,14 @@ public class WebClientResponseException extends WebClientException { /** * Construct a new instance of with the given response data. + * @param message the exception message * @param statusCode the raw status code value - * @param statusText the status text + * @param statusText the status text (may be {@code null}) * @param headers the response headers (may be {@code null}) * @param responseBody the response body content (may be {@code null}) * @param responseCharset the response body charset (may be {@code null}) */ - public WebClientResponseException(String message, int statusCode, String statusText, + public WebClientResponseException(String message, int statusCode, @Nullable String statusText, @Nullable HttpHeaders headers, @Nullable byte[] responseBody, @Nullable Charset responseCharset) { @@ -84,6 +86,7 @@ public int getRawStatusCode() { /** * Return the HTTP status text. */ + @Nullable public String getStatusText() { return this.statusText; } 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 3ed3cd1de025..ea236011fd87 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 @@ -77,6 +77,11 @@ public HttpStatus statusCode() { return this.delegate.statusCode(); } + @Override + public int rawStatusCode() { + return this.delegate.rawStatusCode(); + } + @Override public Headers headers() { return this.delegate.headers(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/HandlerFilterFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/HandlerFilterFunction.java index e2c0ed18ca80..5b6b5d985e80 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/HandlerFilterFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/HandlerFilterFunction.java @@ -74,7 +74,7 @@ default HandlerFunction apply(HandlerFunction handler) { /** * Adapt the given request processor function to a filter function that only operates - * on the {@code ClientRequest}. + * on the {@code ServerRequest}. * @param requestProcessor the request processor * @return the filter adaptation of the request processor */ @@ -87,8 +87,10 @@ default HandlerFunction apply(HandlerFunction handler) { /** * Adapt the given response processor function to a filter function that only operates - * on the {@code ClientResponse}. + * on the {@code ServerResponse}. * @param responseProcessor the response processor + * @param the type of the {@linkplain HandlerFunction handler function} to filter + * @param the type of the response of the function * @return the filter adaptation of the request processor */ static HandlerFilterFunction ofResponseProcessor( diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java index c2ecfd46abaa..92dd40d8c984 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java @@ -34,6 +34,7 @@ /** * @author Arjen Poutsma + * @author Denys Ivano */ public class DefaultClientResponseBuilderTests { @@ -57,6 +58,7 @@ public void normal() { .build(); assertEquals(HttpStatus.BAD_GATEWAY, response.statusCode()); + assertEquals(HttpStatus.BAD_GATEWAY.value(), response.rawStatusCode()); HttpHeaders responseHeaders = response.headers().asHttpHeaders(); assertEquals("bar", responseHeaders.getFirst("foo")); assertNotNull("qux", response.cookies().getFirst("baz")); @@ -67,6 +69,47 @@ public void normal() { .verifyComplete(); } + @Test + public void withRawStatusCode() { + Flux body = Flux.just("baz") + .map(s -> s.getBytes(StandardCharsets.UTF_8)) + .map(dataBufferFactory::wrap); + + ClientResponse response = ClientResponse.create(HttpStatus.BAD_GATEWAY.value(), ExchangeStrategies.withDefaults()) + .body(body) + .build(); + + assertEquals(HttpStatus.BAD_GATEWAY, response.statusCode()); + assertEquals(HttpStatus.BAD_GATEWAY.value(), response.rawStatusCode()); + + StepVerifier.create(response.bodyToFlux(String.class)) + .expectNext("baz") + .verifyComplete(); + } + + @Test + public void withUnknownStatusCode() { + Flux body = Flux.just("baz") + .map(s -> s.getBytes(StandardCharsets.UTF_8)) + .map(dataBufferFactory::wrap); + + ClientResponse response = ClientResponse.create(999, ExchangeStrategies.withDefaults()) + .body(body) + .build(); + + try { + response.statusCode(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + // do nothing + } + assertEquals(999, response.rawStatusCode()); + + StepVerifier.create(response.bodyToFlux(String.class)) + .expectNext("baz") + .verifyComplete(); + } + @Test public void from() throws Exception { Flux otherBody = Flux.just("foo", "bar") @@ -90,6 +133,7 @@ public void from() throws Exception { .build(); assertEquals(HttpStatus.BAD_REQUEST, result.statusCode()); + assertEquals(HttpStatus.BAD_REQUEST.value(), result.rawStatusCode()); assertEquals(1, result.headers().asHttpHeaders().size()); assertEquals("baar", result.headers().asHttpHeaders().getFirst("foo")); assertEquals(1, result.cookies().size()); @@ -100,5 +144,36 @@ public void from() throws Exception { .verifyComplete(); } + @Test + public void fromWithUnknownStatusCode() throws Exception { + Flux otherBody = Flux.just("foo", "bar") + .map(s -> s.getBytes(StandardCharsets.UTF_8)) + .map(dataBufferFactory::wrap); + + ClientResponse other = ClientResponse.create(999, ExchangeStrategies.withDefaults()) + .body(otherBody) + .build(); + + Flux body = Flux.just("baz") + .map(s -> s.getBytes(StandardCharsets.UTF_8)) + .map(dataBufferFactory::wrap); + + ClientResponse result = ClientResponse.from(other) + .body(body) + .build(); + + try { + result.statusCode(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + // do nothing + } + assertEquals(999, result.rawStatusCode()); + + StepVerifier.create(result.bodyToFlux(String.class)) + .expectNext("baz") + .verifyComplete(); + } + } \ No newline at end of file diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java index 913cc368aaa9..f5df56972a4a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,12 +50,14 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.springframework.web.reactive.function.BodyExtractors.toMono; /** * @author Arjen Poutsma + * @author Denys Ivano */ public class DefaultClientResponseTests { @@ -82,6 +84,14 @@ public void statusCode() throws Exception { assertEquals(status, defaultClientResponse.statusCode()); } + @Test + public void rawStatusCode() throws Exception { + int status = 999; + when(mockResponse.getRawStatusCode()).thenReturn(status); + + assertEquals(status, defaultClientResponse.rawStatusCode()); + } + @Test public void header() throws Exception { HttpHeaders httpHeaders = new HttpHeaders(); @@ -145,6 +155,7 @@ public void bodyToMono() throws Exception { httpHeaders.setContentType(MediaType.TEXT_PLAIN); when(mockResponse.getHeaders()).thenReturn(httpHeaders); when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getRawStatusCode()).thenReturn(HttpStatus.OK.value()); when(mockResponse.getBody()).thenReturn(body); List> messageReaders = Collections @@ -166,6 +177,7 @@ public void bodyToMonoTypeReference() throws Exception { httpHeaders.setContentType(MediaType.TEXT_PLAIN); when(mockResponse.getHeaders()).thenReturn(httpHeaders); when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getRawStatusCode()).thenReturn(HttpStatus.OK.value()); when(mockResponse.getBody()).thenReturn(body); List> messageReaders = Collections @@ -189,6 +201,7 @@ public void bodyToFlux() throws Exception { httpHeaders.setContentType(MediaType.TEXT_PLAIN); when(mockResponse.getHeaders()).thenReturn(httpHeaders); when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getRawStatusCode()).thenReturn(HttpStatus.OK.value()); when(mockResponse.getBody()).thenReturn(body); List> messageReaders = Collections @@ -211,6 +224,7 @@ public void bodyToFluxTypeReference() throws Exception { httpHeaders.setContentType(MediaType.TEXT_PLAIN); when(mockResponse.getHeaders()).thenReturn(httpHeaders); when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getRawStatusCode()).thenReturn(HttpStatus.OK.value()); when(mockResponse.getBody()).thenReturn(body); List> messageReaders = Collections @@ -235,6 +249,7 @@ public void toEntity() throws Exception { httpHeaders.setContentType(MediaType.TEXT_PLAIN); when(mockResponse.getHeaders()).thenReturn(httpHeaders); when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getRawStatusCode()).thenReturn(HttpStatus.OK.value()); when(mockResponse.getBody()).thenReturn(body); List> messageReaders = Collections @@ -244,6 +259,37 @@ public void toEntity() throws Exception { ResponseEntity result = defaultClientResponse.toEntity(String.class).block(); assertEquals("foo", result.getBody()); assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals(HttpStatus.OK.value(), result.getStatusCodeValue()); + assertEquals(MediaType.TEXT_PLAIN, result.getHeaders().getContentType()); + } + + @Test + public void toEntityWithUnknownStatusCode() throws Exception { + DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); + DefaultDataBuffer dataBuffer + = factory.wrap(ByteBuffer.wrap("foo".getBytes(StandardCharsets.UTF_8))); + Flux body = Flux.just(dataBuffer); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + when(mockResponse.getHeaders()).thenReturn(httpHeaders); + when(mockResponse.getStatusCode()).thenThrow(new IllegalArgumentException("999")); + when(mockResponse.getRawStatusCode()).thenReturn(999); + when(mockResponse.getBody()).thenReturn(body); + + List> messageReaders = Collections + .singletonList(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes())); + when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders); + + ResponseEntity result = defaultClientResponse.toEntity(String.class).block(); + assertEquals("foo", result.getBody()); + try { + result.getStatusCode(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + // do nothing + } + assertEquals(999, result.getStatusCodeValue()); assertEquals(MediaType.TEXT_PLAIN, result.getHeaders().getContentType()); } @@ -258,6 +304,7 @@ public void toEntityTypeReference() throws Exception { httpHeaders.setContentType(MediaType.TEXT_PLAIN); when(mockResponse.getHeaders()).thenReturn(httpHeaders); when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getRawStatusCode()).thenReturn(HttpStatus.OK.value()); when(mockResponse.getBody()).thenReturn(body); List> messageReaders = Collections @@ -269,6 +316,7 @@ public void toEntityTypeReference() throws Exception { }).block(); assertEquals("foo", result.getBody()); assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals(HttpStatus.OK.value(), result.getStatusCodeValue()); assertEquals(MediaType.TEXT_PLAIN, result.getHeaders().getContentType()); } @@ -283,6 +331,7 @@ public void toEntityList() throws Exception { httpHeaders.setContentType(MediaType.TEXT_PLAIN); when(mockResponse.getHeaders()).thenReturn(httpHeaders); when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getRawStatusCode()).thenReturn(HttpStatus.OK.value()); when(mockResponse.getBody()).thenReturn(body); List> messageReaders = Collections @@ -292,6 +341,37 @@ public void toEntityList() throws Exception { ResponseEntity> result = defaultClientResponse.toEntityList(String.class).block(); assertEquals(Collections.singletonList("foo"), result.getBody()); assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals(HttpStatus.OK.value(), result.getStatusCodeValue()); + assertEquals(MediaType.TEXT_PLAIN, result.getHeaders().getContentType()); + } + + @Test + public void toEntityListWithUnknownStatusCode() throws Exception { + DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); + DefaultDataBuffer dataBuffer = + factory.wrap(ByteBuffer.wrap("foo".getBytes(StandardCharsets.UTF_8))); + Flux body = Flux.just(dataBuffer); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + when(mockResponse.getHeaders()).thenReturn(httpHeaders); + when(mockResponse.getStatusCode()).thenThrow(new IllegalArgumentException("999")); + when(mockResponse.getRawStatusCode()).thenReturn(999); + when(mockResponse.getBody()).thenReturn(body); + + List> messageReaders = Collections + .singletonList(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes())); + when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders); + + ResponseEntity> result = defaultClientResponse.toEntityList(String.class).block(); + assertEquals(Collections.singletonList("foo"), result.getBody()); + try { + result.getStatusCode(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + // do nothing + } + assertEquals(999, result.getStatusCodeValue()); assertEquals(MediaType.TEXT_PLAIN, result.getHeaders().getContentType()); } @@ -306,6 +386,7 @@ public void toEntityListTypeReference() throws Exception { httpHeaders.setContentType(MediaType.TEXT_PLAIN); when(mockResponse.getHeaders()).thenReturn(httpHeaders); when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getRawStatusCode()).thenReturn(HttpStatus.OK.value()); when(mockResponse.getBody()).thenReturn(body); List> messageReaders = Collections @@ -317,6 +398,7 @@ public void toEntityListTypeReference() throws Exception { }).block(); assertEquals(Collections.singletonList("foo"), result.getBody()); assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals(HttpStatus.OK.value(), result.getStatusCodeValue()); assertEquals(MediaType.TEXT_PLAIN, result.getHeaders().getContentType()); } @@ -328,6 +410,7 @@ public void toMonoVoid() throws Exception { httpHeaders.setContentType(MediaType.TEXT_PLAIN); when(mockResponse.getHeaders()).thenReturn(httpHeaders); when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getRawStatusCode()).thenReturn(HttpStatus.OK.value()); when(mockResponse.getBody()).thenReturn(body.flux()); List> messageReaders = Collections @@ -353,6 +436,7 @@ public void toMonoVoidNonEmptyBody() throws Exception { httpHeaders.setContentType(MediaType.TEXT_PLAIN); when(mockResponse.getHeaders()).thenReturn(httpHeaders); when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getRawStatusCode()).thenReturn(HttpStatus.OK.value()); when(mockResponse.getBody()).thenReturn(body.flux()); List> messageReaders = Collections diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java index a5ba2dd3b3f2..913781d5eba0 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java @@ -32,6 +32,7 @@ /** * @author Arjen Poutsma + * @author Denys Ivano */ public class ExchangeFilterFunctionsTests { @@ -147,6 +148,7 @@ public void statusHandlerMatch() { ClientRequest request = ClientRequest.create(GET, URI.create("http://example.com")).build(); ClientResponse response = mock(ClientResponse.class); when(response.statusCode()).thenReturn(HttpStatus.NOT_FOUND); + when(response.rawStatusCode()).thenReturn(HttpStatus.NOT_FOUND.value()); ExchangeFunction exchange = r -> Mono.just(response); @@ -165,6 +167,7 @@ public void statusHandlerNoMatch() { ClientRequest request = ClientRequest.create(GET, URI.create("http://example.com")).build(); ClientResponse response = mock(ClientResponse.class); when(response.statusCode()).thenReturn(HttpStatus.NOT_FOUND); + when(response.rawStatusCode()).thenReturn(HttpStatus.NOT_FOUND.value()); ExchangeFunction exchange = r -> Mono.just(response); @@ -179,6 +182,85 @@ public void statusHandlerNoMatch() { .verify(); } + @Test + public void statusHandlerUnknownStatusCode() { + ClientRequest request = ClientRequest.create(GET, URI.create("http://example.com")).build(); + ClientResponse response = mock(ClientResponse.class); + when(response.statusCode()).thenThrow(new IllegalArgumentException("999")); + when(response.rawStatusCode()).thenReturn(999); + + ExchangeFunction exchange = r -> Mono.just(response); + + ExchangeFilterFunction errorHandler = ExchangeFilterFunctions.statusError( + HttpStatus::is5xxServerError, r -> new MyException()); + + Mono result = errorHandler.filter(request, exchange); + + StepVerifier.create(result) + .expectNext(response) + .expectComplete() + .verify(); + } + + @Test + public void statusCodeHandlerMatch() { + ClientRequest request = ClientRequest.create(GET, URI.create("http://example.com")).build(); + ClientResponse response = mock(ClientResponse.class); + when(response.statusCode()).thenReturn(HttpStatus.NOT_FOUND); + when(response.rawStatusCode()).thenReturn(HttpStatus.NOT_FOUND.value()); + + ExchangeFunction exchange = r -> Mono.just(response); + + ExchangeFilterFunction errorHandler = ExchangeFilterFunctions.statusCodeError( + StatusCodePredicates.is4xxClientError(), r -> new MyException()); + + Mono result = errorHandler.filter(request, exchange); + + StepVerifier.create(result) + .expectError(MyException.class) + .verify(); + } + + @Test + public void statusCodeHandlerNoMatch() { + ClientRequest request = ClientRequest.create(GET, URI.create("http://example.com")).build(); + ClientResponse response = mock(ClientResponse.class); + when(response.statusCode()).thenReturn(HttpStatus.NOT_FOUND); + when(response.rawStatusCode()).thenReturn(HttpStatus.NOT_FOUND.value()); + + ExchangeFunction exchange = r -> Mono.just(response); + + ExchangeFilterFunction errorHandler = ExchangeFilterFunctions.statusCodeError( + StatusCodePredicates.is5xxServerError(), r -> new MyException()); + + Mono result = errorHandler.filter(request, exchange); + + StepVerifier.create(result) + .expectNext(response) + .expectComplete() + .verify(); + } + + @Test + public void statusCodeHandlerUnknownStatusCode() { + ClientRequest request = ClientRequest.create(GET, URI.create("http://example.com")).build(); + ClientResponse response = mock(ClientResponse.class); + when(response.statusCode()).thenThrow(new IllegalArgumentException("999")); + when(response.rawStatusCode()).thenReturn(999); + + ExchangeFunction exchange = r -> Mono.just(response); + + ExchangeFilterFunction errorHandler = ExchangeFilterFunctions.statusCodeError( + StatusCodePredicates.is5xxServerError(), r -> new MyException()); + + Mono result = errorHandler.filter(request, exchange); + + StepVerifier.create(result) + .expectNext(response) + .expectComplete() + .verify(); + } + @SuppressWarnings("serial") private static class MyException extends Exception { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/StatusCodePredicatesTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/StatusCodePredicatesTests.java new file mode 100644 index 000000000000..08e998cf1579 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/StatusCodePredicatesTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.function.client; + +import java.util.function.Predicate; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Denys Ivano + */ +public class StatusCodePredicatesTests { + + @Test + public void is1xxInformational() { + Predicate predicate = StatusCodePredicates.is1xxInformational(); + + assertTrue(predicate.test(100)); + assertTrue(predicate.test(101)); + assertTrue(predicate.test(199)); + assertFalse(predicate.test(200)); + } + + @Test + public void is2xxSuccessful() { + Predicate predicate = StatusCodePredicates.is2xxSuccessful(); + + assertTrue(predicate.test(200)); + assertTrue(predicate.test(204)); + assertTrue(predicate.test(299)); + assertFalse(predicate.test(300)); + } + + @Test + public void is3xxRedirection() { + Predicate predicate = StatusCodePredicates.is3xxRedirection(); + + assertTrue(predicate.test(300)); + assertTrue(predicate.test(302)); + assertTrue(predicate.test(399)); + assertFalse(predicate.test(400)); + } + + @Test + public void is4xxClientError() { + Predicate predicate = StatusCodePredicates.is4xxClientError(); + + assertTrue(predicate.test(400)); + assertTrue(predicate.test(404)); + assertTrue(predicate.test(499)); + assertFalse(predicate.test(500)); + } + + @Test + public void is5xxServerError() { + Predicate predicate = StatusCodePredicates.is5xxServerError(); + + assertTrue(predicate.test(500)); + assertTrue(predicate.test(502)); + assertTrue(predicate.test(599)); + assertFalse(predicate.test(600)); + } + + @Test + public void isError() { + Predicate predicate = StatusCodePredicates.isError(); + + assertTrue(predicate.test(400)); + assertTrue(predicate.test(404)); + assertTrue(predicate.test(502)); + assertFalse(predicate.test(200)); + assertFalse(predicate.test(999)); + } + + @Test + public void between() { + assertTrue(StatusCodePredicates.between(100, 200).test(100)); + assertTrue(StatusCodePredicates.between(100, 200).test(200)); + assertTrue(StatusCodePredicates.between(400, 499).test(404)); + assertFalse(StatusCodePredicates.between(400, 400).test(401)); + assertFalse(StatusCodePredicates.between(400, 499).test(500)); + try { + StatusCodePredicates.between(400, 399); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + // do nothing + } + } + + @Test + public void is() { + assertTrue(StatusCodePredicates.is(400).test(400)); + assertFalse(StatusCodePredicates.is(400).test(500)); + } + + @Test + public void not() { + assertTrue(StatusCodePredicates.not(200).test(400)); + assertFalse(StatusCodePredicates.not(200).test(200)); + } + + @Test + public void anyOf() { + assertTrue(StatusCodePredicates.anyOf(400, 401, 403).test(401)); + assertTrue(StatusCodePredicates.anyOf(500).test(500)); + assertFalse(StatusCodePredicates.anyOf(new int[0]).test(500)); + assertFalse(StatusCodePredicates.anyOf(400, 500).test(404)); + } + + @Test + public void noneOf() { + assertTrue(StatusCodePredicates.noneOf(new int[0]).test(500)); + assertTrue(StatusCodePredicates.noneOf(200, 204).test(400)); + assertFalse(StatusCodePredicates.noneOf(500).test(500)); + assertFalse(StatusCodePredicates.noneOf(200, 204, 302).test(302)); + } + +} 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 f0671812d5e1..4c912de498c9 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 @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.List; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.zip.CRC32; import okhttp3.mockwebserver.MockResponse; @@ -55,6 +56,7 @@ * * @author Brian Clozel * @author Rossen Stoyanchev + * @author Denys Ivano */ public class WebClientIntegrationTests { @@ -411,7 +413,7 @@ public void shouldGetErrorSignalOn404() throws Exception { .bodyToMono(String.class); StepVerifier.create(result) - .expectError(WebClientException.class) + .expectError(WebClientResponseException.class) .verify(Duration.ofSeconds(3)); expectRequestCount(1); @@ -431,7 +433,7 @@ public void shouldGetErrorSignalOnEmptyErrorResponse() throws Exception { .bodyToMono(String.class); StepVerifier.create(result) - .expectError(WebClientException.class) + .expectError(WebClientResponseException.class) .verify(Duration.ofSeconds(3)); expectRequestCount(1); @@ -457,6 +459,9 @@ public void shouldGetInternalServerErrorSignal() throws Exception { assertTrue(throwable instanceof WebClientResponseException); WebClientResponseException ex = (WebClientResponseException) throwable; assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, ex.getStatusCode()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getRawStatusCode()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), + ex.getStatusText()); assertEquals(MediaType.TEXT_PLAIN, ex.getHeaders().getContentType()); assertEquals(errorMessage, ex.getResponseBodyAsString()); }) @@ -469,6 +474,38 @@ public void shouldGetInternalServerErrorSignal() throws Exception { }); } + @Test + public void shouldGetErrorSignalOnUnknownErrorStatusCode() throws Exception { + int errorStatus = 555; // 4xx or 5xx + assertNull(HttpStatus.resolve(errorStatus)); + + String errorMessage = "Something went wrong"; + prepareResponse(response -> response.setResponseCode(errorStatus) + .setHeader("Content-Type", "text/plain").setBody(errorMessage)); + + Mono result = this.webClient.get() + .uri("/unknownPage") + .retrieve() + .bodyToMono(String.class); + + StepVerifier.create(result) + .expectErrorSatisfies(throwable -> { + assertTrue(throwable instanceof WebClientResponseException); + WebClientResponseException ex = (WebClientResponseException) throwable; + assertEquals(errorStatus, ex.getRawStatusCode()); + assertNull(ex.getStatusText()); + assertEquals(MediaType.TEXT_PLAIN, ex.getHeaders().getContentType()); + assertEquals(errorMessage, ex.getResponseBodyAsString()); + }) + .verify(Duration.ofSeconds(3)); + + expectRequestCount(1); + expectRequest(request -> { + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/unknownPage", request.getPath()); + }); + } + @Test public void shouldApplyCustomStatusHandler() throws Exception { prepareResponse(response -> response.setResponseCode(500) @@ -513,6 +550,100 @@ public void shouldApplyCustomStatusHandlerParameterizedTypeReference() throws Ex }); } + @Test + public void shouldApplyCustomStatusCodeHandler() throws Exception { + int status = 999; + + prepareResponse(response -> response.setResponseCode(status) + .setHeader("Content-Type", "text/plain").setBody("Something went wrong")); + + Mono result = this.webClient.get() + .uri("/unknownServerPage") + .retrieve() + .onStatusCode(StatusCodePredicates.between(600, 999), response -> Mono.just(new MyException("Error!"))) + .bodyToMono(String.class); + + StepVerifier.create(result) + .expectError(MyException.class) + .verify(Duration.ofSeconds(3)); + + expectRequestCount(1); + expectRequest(request -> { + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/unknownServerPage", request.getPath()); + }); + } + + @Test + public void shouldApplyCustomStatusAndStatusCodeHandlers() throws Exception { + Supplier responseSpec = () -> { + return this.webClient.get() + .uri("/greeting?name=Spring") + .retrieve() + .onStatus(HttpStatus::is4xxClientError, response -> Mono.just(new MyException("HttpStatus 4xx"))) + .onStatusCode(StatusCodePredicates.is5xxServerError(), response -> Mono.just(new MyException("Status code 5xx"))) + // this handler shouldn't be called + .onStatus(HttpStatus::is5xxServerError, response -> Mono.just(new MyException("HttpStatus 5xx"))) + .onStatusCode(StatusCodePredicates.between(600, 999), response -> Mono.just(new MyException("Status code >= 600"))); + }; + + // 400 -> "HttpStatus 4xx" + prepareResponse(response -> response.setResponseCode(400) + .setHeader("Content-Type", "text/plain").setBody("Something went wrong")); + + Mono result = responseSpec.get().bodyToMono(String.class); + + StepVerifier.create(result) + .expectErrorSatisfies(throwable -> { + assertTrue(throwable instanceof MyException); + assertEquals("HttpStatus 4xx", throwable.getMessage()); + }) + .verify(Duration.ofSeconds(3)); + + expectRequest(request -> { + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/greeting?name=Spring", request.getPath()); + }); + + // 500 -> "Status code 5xx" + prepareResponse(response -> response.setResponseCode(500) + .setHeader("Content-Type", "text/plain").setBody("Something went wrong")); + + result = responseSpec.get().bodyToMono(String.class); + + StepVerifier.create(result) + .expectErrorSatisfies(throwable -> { + assertTrue(throwable instanceof MyException); + assertEquals("Status code 5xx", throwable.getMessage()); + }) + .verify(Duration.ofSeconds(3)); + + expectRequest(request -> { + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/greeting?name=Spring", request.getPath()); + }); + + // 999 -> "Status code >= 600" + prepareResponse(response -> response.setResponseCode(999) + .setHeader("Content-Type", "text/plain").setBody("Something went wrong")); + + result = responseSpec.get().bodyToMono(String.class); + + StepVerifier.create(result) + .expectErrorSatisfies(throwable -> { + assertTrue(throwable instanceof MyException); + assertEquals("Status code >= 600", throwable.getMessage()); + }) + .verify(Duration.ofSeconds(3)); + + expectRequest(request -> { + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/greeting?name=Spring", request.getPath()); + }); + + expectRequestCount(3); + } + @Test public void shouldReceiveNotFoundEntity() throws Exception { prepareResponse(response -> response.setResponseCode(404) @@ -605,6 +736,61 @@ public void shouldApplyErrorHandlingFilter() throws Exception { expectRequestCount(2); } + @Test + public void shouldSupportUnknownStatusCodeOnExchange() throws Exception { + int status = 999; + assertNull(HttpStatus.resolve(status)); + + prepareResponse(response -> response.setResponseCode(status) + .setHeader("Content-Type", "text/plain") + .setBody("Hello Spring!")); + + Mono result = this.webClient.get() + .uri("/unknownStatusCode") + .exchange() + .flatMap(r -> { + assertEquals(status, r.rawStatusCode()); + return r.bodyToMono(String.class); + }); + + StepVerifier.create(result) + .expectNext("Hello Spring!") + .expectComplete() + .verify(Duration.ofSeconds(3)); + + expectRequestCount(1); + expectRequest(request -> { + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/unknownStatusCode", request.getPath()); + }); + } + + @Test + public void shouldSupportUnknownStatusCodeOnRetrieve() throws Exception { + int status = 999; + assertNull(HttpStatus.resolve(status)); + + prepareResponse(response -> response.setResponseCode(status) + .setHeader("Content-Type", "text/plain") + .setBody("Hello Spring!")); + + Mono result = this.webClient.get() + .uri("/unknownStatusCode") + .retrieve() + .bodyToMono(String.class); + + StepVerifier.create(result) + .expectNext("Hello Spring!") + .expectComplete() + .verify(Duration.ofSeconds(3)); + + expectRequestCount(1); + expectRequest(request -> { + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/unknownStatusCode", request.getPath()); + }); + } + @Test public void shouldReceiveEmptyResponse() throws Exception { prepareResponse(response -> response.setHeader("Content-Length", "0").setBody("")); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapperTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapperTests.java index 6937bb761e52..590b6c09ad85 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapperTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapperTests.java @@ -65,6 +65,14 @@ public void statusCode() throws Exception { assertSame(status, wrapper.statusCode()); } + @Test + public void rawStatusCode() throws Exception { + int status = 999; + when(mockResponse.rawStatusCode()).thenReturn(status); + + assertEquals(status, wrapper.rawStatusCode()); + } + @Test public void headers() throws Exception { ClientResponse.Headers headers = mock(ClientResponse.Headers.class); diff --git a/src/docs/asciidoc/web/webflux-webclient.adoc b/src/docs/asciidoc/web/webflux-webclient.adoc index c077b750e8d4..73f3cba2dbfe 100644 --- a/src/docs/asciidoc/web/webflux-webclient.adoc +++ b/src/docs/asciidoc/web/webflux-webclient.adoc @@ -66,8 +66,8 @@ By default, responses with 4xx or 5xx status codes result in an error of type Mono result = client.get() .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) .retrieve() - .onStatus(HttpStatus::is4xxServerError, response -> ...) - .onStatus(HttpStatus::is5xxServerError, response -> ...) + .onStatus(HttpStatus::is4xxClientError, response -> ...) + .onStatusCode(StatusCodePredicates.is(500), response -> ...) .bodyToMono(Person.class); ----