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

@@ -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.
@@ -54,6 +54,7 @@ import static org.springframework.web.reactive.function.BodyExtractors.*;
/**
* @author Arjen Poutsma
* @author Denys Ivano
*/
public class DefaultClientResponseTests {
@@ -80,6 +81,14 @@ public class DefaultClientResponseTests {
assertEquals(status, defaultClientResponse.statusCode());
}
@Test
public void rawStatusCode() {
int status = 999;
when(mockResponse.getRawStatusCode()).thenReturn(status);
assertEquals(status, defaultClientResponse.rawStatusCode());
}
@Test
public void header() {
HttpHeaders httpHeaders = new HttpHeaders();
@@ -143,6 +152,7 @@ public class DefaultClientResponseTests {
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<HttpMessageReader<?>> messageReaders = Collections
@@ -164,6 +174,7 @@ public class DefaultClientResponseTests {
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<HttpMessageReader<?>> messageReaders = Collections
@@ -187,6 +198,7 @@ public class DefaultClientResponseTests {
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<HttpMessageReader<?>> messageReaders = Collections
@@ -209,6 +221,7 @@ public class DefaultClientResponseTests {
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<HttpMessageReader<?>> messageReaders = Collections
@@ -233,6 +246,7 @@ public class DefaultClientResponseTests {
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<HttpMessageReader<?>> messageReaders = Collections
@@ -242,6 +256,37 @@ public class DefaultClientResponseTests {
ResponseEntity<String> 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<DataBuffer> 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<HttpMessageReader<?>> messageReaders = Collections
.singletonList(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes()));
when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders);
ResponseEntity<String> 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());
}
@@ -256,6 +301,7 @@ public class DefaultClientResponseTests {
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<HttpMessageReader<?>> messageReaders = Collections
@@ -267,6 +313,7 @@ public class DefaultClientResponseTests {
}).block();
assertEquals("foo", result.getBody());
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals(HttpStatus.OK.value(), result.getStatusCodeValue());
assertEquals(MediaType.TEXT_PLAIN, result.getHeaders().getContentType());
}
@@ -281,6 +328,7 @@ public class DefaultClientResponseTests {
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<HttpMessageReader<?>> messageReaders = Collections
@@ -290,6 +338,37 @@ public class DefaultClientResponseTests {
ResponseEntity<List<String>> 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<DataBuffer> 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<HttpMessageReader<?>> messageReaders = Collections
.singletonList(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes()));
when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders);
ResponseEntity<List<String>> 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());
}
@@ -304,6 +383,7 @@ public class DefaultClientResponseTests {
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<HttpMessageReader<?>> messageReaders = Collections
@@ -315,6 +395,7 @@ public class DefaultClientResponseTests {
}).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());
}
@@ -326,6 +407,7 @@ public class DefaultClientResponseTests {
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<HttpMessageReader<?>> messageReaders = Collections
@@ -351,6 +433,7 @@ public class DefaultClientResponseTests {
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<HttpMessageReader<?>> messageReaders = Collections

View File

@@ -55,6 +55,7 @@ import static org.junit.Assert.*;
*
* @author Brian Clozel
* @author Rossen Stoyanchev
* @author Denys Ivano
*/
public class WebClientIntegrationTests {
@@ -413,7 +414,7 @@ public class WebClientIntegrationTests {
.bodyToMono(String.class);
StepVerifier.create(result)
.expectError(WebClientException.class)
.expectError(WebClientResponseException.class)
.verify(Duration.ofSeconds(3));
expectRequestCount(1);
@@ -433,7 +434,7 @@ public class WebClientIntegrationTests {
.bodyToMono(String.class);
StepVerifier.create(result)
.expectError(WebClientException.class)
.expectError(WebClientResponseException.class)
.verify(Duration.ofSeconds(3));
expectRequestCount(1);
@@ -459,6 +460,9 @@ public class WebClientIntegrationTests {
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());
})
@@ -471,6 +475,62 @@ public class WebClientIntegrationTests {
});
}
@Test
public void shouldSupportUnknownStatusCode() {
int errorStatus = 555;
assertNull(HttpStatus.resolve(errorStatus));
String errorMessage = "Something went wrong";
prepareResponse(response -> response.setResponseCode(errorStatus)
.setHeader("Content-Type", "text/plain").setBody(errorMessage));
Mono<ClientResponse> result = this.webClient.get()
.uri("/unknownPage")
.exchange();
StepVerifier.create(result)
.consumeNextWith(response -> assertEquals(555, response.rawStatusCode()))
.expectComplete()
.verify(Duration.ofSeconds(3));
expectRequestCount(1);
expectRequest(request -> {
assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT));
assertEquals("/unknownPage", request.getPath());
});
}
@Test
public void shouldGetErrorSignalWhenRetrievingUnknownStatusCode() {
int errorStatus = 555;
assertNull(HttpStatus.resolve(errorStatus));
String errorMessage = "Something went wrong";
prepareResponse(response -> response.setResponseCode(errorStatus)
.setHeader("Content-Type", "text/plain").setBody(errorMessage));
Mono<String> result = this.webClient.get()
.uri("/unknownPage")
.retrieve()
.bodyToMono(String.class);
StepVerifier.create(result)
.expectErrorSatisfies(throwable -> {
assertTrue(throwable instanceof UnknownHttpStatusCodeException);
UnknownHttpStatusCodeException ex = (UnknownHttpStatusCodeException) throwable;
assertEquals("Unknown status code ["+errorStatus+"]", ex.getMessage());
assertEquals(errorStatus, ex.getRawStatusCode());
assertEquals("", 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() {
prepareResponse(response -> response.setResponseCode(500)

View File

@@ -65,6 +65,14 @@ public class ClientResponseWrapperTests {
assertSame(status, wrapper.statusCode());
}
@Test
public void rawStatusCode() {
int status = 999;
when(mockResponse.rawStatusCode()).thenReturn(status);
assertEquals(status, wrapper.rawStatusCode());
}
@Test
public void headers() {
ClientResponse.Headers headers = mock(ClientResponse.Headers.class);