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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user