Improve unknown status codes handling by WebClient

Prior to this commit, `WebClient` would throw `IllegalArgumentException`
when receiving an HTTP response with an unknown HTTP status code.

This commit is a follow up of SPR-16748 (supporting non-standard HTTP
status codes on the reactive `ClientHttpResponse`), and is mirroring
SPR-15978 (supporting non-standard HTTP status codes in `RestTemplate`).

With this change, `WebClient` now tolerates unknown status codes in some
cases, while not offering that choice as a first class citizen:
`HttpStatus` is still the preferred way to deal with HTTP status codes.

Here's how `WebClient` will behave when fetching the full response:

```
// Given a remote endpoint returning a "123" HTTP status code
Mono<ClientResponse> result = this.webClient.get()
				.uri("/status/123")
				.exchange();

// will still throw an IllegalArgumentException
HttpStatus status = result.block().statusCode();

// is safe and will return 123
int statusCode = result.block().rawStatusCode();
```

Resolving directly the response body with `retrieve()` is different.

```
// will send an error signal with a UnknownHttpStatusCodeException
Mono<String> result = this.webClient.get()
				.uri("/status/123")
				.retrieve()
				.bodyToMono(String.class);
```

In general, `WebClient` will provide high-level support
for well-known HTTP status codes, like error handling with
`WebClient.ResponseSpec#onStatus`.

For such support with unknown status codes, it is better to rely
on lower level constructs such as `ExchangeFilterFunction`.

Issue: SPR-16819
This commit is contained in:
Brian Clozel
2018-07-12 19:08:08 +02:00
parent f123d6c3bc
commit 038af9a303
10 changed files with 253 additions and 28 deletions

View File

@@ -60,9 +60,21 @@ 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 (potentially non-standard) status code of this response.
* @return the status as an integer
* @since 5.1
* @see #statusCode()
* @see HttpStatus#resolve(int)
*/
int rawStatusCode();
/**
* Return the headers of this response.
*/

View File

@@ -76,6 +76,11 @@ class DefaultClientResponse implements ClientResponse {
return this.response.getStatusCode();
}
@Override
public int rawStatusCode() {
return this.response.getRawStatusCode();
}
@Override
public Headers headers() {
return this.headers;
@@ -177,11 +182,11 @@ class DefaultClientResponse implements ClientResponse {
private <T> Mono<ResponseEntity<T>> toEntityInternal(Mono<T> 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
@@ -196,10 +201,24 @@ class DefaultClientResponse implements ClientResponse {
private <T> Mono<ResponseEntity<List<T>>> toEntityListInternal(Flux<T> 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 <T> ResponseEntity<T> createEntity(HttpHeaders headers, int status) {
HttpStatus resolvedStatus = HttpStatus.resolve(status);
return resolvedStatus != null
? new ResponseEntity<>(headers, resolvedStatus)
: ResponseEntity.status(status).headers(headers).build();
}
private <T> ResponseEntity<T> 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);
}

View File

@@ -59,6 +59,7 @@ import org.springframework.web.util.UriBuilderFactory;
* Default implementation of {@link WebClient}.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0
*/
class DefaultWebClient implements WebClient {
@@ -375,7 +376,7 @@ class DefaultWebClient implements WebClient {
private final Mono<ClientResponse> responseMono;
private List<StatusHandler> statusHandlers = new ArrayList<>(1);
private final List<StatusHandler> statusHandlers = new ArrayList<>(1);
DefaultResponseSpec(Mono<ClientResponse> responseMono) {
this.responseMono = responseMono;
@@ -435,13 +436,17 @@ class DefaultWebClient implements WebClient {
private <T extends Publisher<?>> T bodyToPublisher(ClientResponse response,
T bodyPublisher, Function<Mono<? extends Throwable>, T> errorFunction) {
return this.statusHandlers.stream()
.filter(statusHandler -> statusHandler.test(response.statusCode()))
.findFirst()
.map(statusHandler -> statusHandler.apply(response))
.map(errorFunction::apply)
.orElse(bodyPublisher);
if (HttpStatus.resolve(response.rawStatusCode()) != null) {
return this.statusHandlers.stream()
.filter(statusHandler -> statusHandler.test(response.statusCode()))
.findFirst()
.map(statusHandler -> statusHandler.apply(response))
.map(errorFunction::apply)
.orElse(bodyPublisher);
}
else {
return errorFunction.apply(createResponseException(response));
}
}
private static Mono<WebClientResponseException> createResponseException(ClientResponse response) {
@@ -454,18 +459,26 @@ class DefaultWebClient implements WebClient {
})
.defaultIfEmpty(new byte[0])
.map(bodyBytes -> {
String msg = String.format("ClientResponse has erroneous status code: %d %s", response.statusCode().value(),
response.statusCode().getReasonPhrase());
Charset charset = response.headers().contentType()
.map(MimeType::getCharset)
.orElse(StandardCharsets.ISO_8859_1);
return new WebClientResponseException(msg,
response.statusCode().value(),
response.statusCode().getReasonPhrase(),
response.headers().asHttpHeaders(),
bodyBytes,
charset
);
if (HttpStatus.resolve(response.rawStatusCode()) != null) {
String msg = String.format("ClientResponse has erroneous status code: %d %s",
response.statusCode().value(), response.statusCode().getReasonPhrase());
return new WebClientResponseException(msg,
response.statusCode().value(),
response.statusCode().getReasonPhrase(),
response.headers().asHttpHeaders(),
bodyBytes,
charset);
}
else {
return new UnknownHttpStatusCodeException(
response.rawStatusCode(),
response.headers().asHttpHeaders(),
bodyBytes,
charset);
}
});
}

View File

@@ -0,0 +1,23 @@
package org.springframework.web.reactive.function.client;
import java.nio.charset.Charset;
import org.springframework.http.HttpHeaders;
/**
* Exception thrown when an unknown (or custom) HTTP status code is received.
*
* @author Brian Clozel
* @since 5.1
*/
public class UnknownHttpStatusCodeException extends WebClientResponseException {
private static final long serialVersionUID = 2407169540168185007L;
public UnknownHttpStatusCodeException(int statusCode, HttpHeaders headers,
byte[] responseBody, Charset responseCharset) {
super("Unknown status code [" + statusCode + "]", statusCode, "",
headers, responseBody, responseCharset);
}
}

View File

@@ -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.
@@ -47,6 +47,7 @@ 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 headers the response headers (may be {@code null})
@@ -69,6 +70,7 @@ public class WebClientResponseException extends WebClientException {
/**
* Return the HTTP status code value.
* @throws IllegalArgumentException in case of an unknown HTTP status code
*/
public HttpStatus getStatusCode() {
return HttpStatus.valueOf(this.statusCode);

View File

@@ -77,6 +77,11 @@ public class ClientResponseWrapper implements ClientResponse {
return this.delegate.statusCode();
}
@Override
public int rawStatusCode() {
return this.delegate.rawStatusCode();
}
@Override
public Headers headers() {
return this.delegate.headers();

View File

@@ -74,7 +74,7 @@ public interface HandlerFilterFunction<T extends ServerResponse, R extends Serve
/**
* 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,7 +87,7 @@ public interface HandlerFilterFunction<T extends ServerResponse, R extends Serve
/**
* 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
* @return the filter adaptation of the request processor
*/