Reputation: 7496
I am trying to replace the existing client code with RestTemplate
with a WebClient
. For that reason, most of the calls need to be blocking, so that the main portion of the application does not need to change. When it comes to error handling this poses a bit of a problem. There are several cases that have to be covered:
List
matching the type of a successful responseIn order to produce the correct error (Exception
) the error response needs to be considered. So far I am unable to get my hands on the error body.
I am using this RestController
method to produce the error response:
@GetMapping("/error/404")
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity error404() {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse());
}
With this response object:
public class ErrorResponse {
private String message = "Error message";
public String getMessage() {
return message;
}
}
The WebClient
is defined as follows:
WebClient.builder()
.baseUrl("http://localhost:8081")
.clientConnector(connector)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
With the connector being of type CloseableHttpAsyncClient
(Apache Http client5).
From my test application I make the call like this:
public String get(int httpStatus) {
try {
return webClient.get()
.uri("/error/" + httpStatus)
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> {
clientResponse.bodyToMono(String.class).flatMap(responseBody -> {
log.error("Body from within flatMap within onStatus: {}", responseBody);
return Mono.just(responseBody);
});
return Mono.error(new RuntimeException("Resolved!"));
})
.bodyToMono(String.class)
.flatMap(clientResponse -> {
log.warn("Body from within flatMap: {}", clientResponse);
return Mono.just(clientResponse);
})
.block();
} catch (Exception ex) {
log.error("Caught Error: ", ex);
return ex.getMessage();
}
}
What I get is the RuntimeException
from the onStatus
return and of course the caught exception in the end.
I am missing the processing from the bodyToMono
from within the onStatus
. My suspicion is that this is not executed due to the blocking nature, as the response body is dealt with the bodyToMono
after the onStatus
.
When commenting out the onStatus
I would expect that we log the warning in the flatMap
, which does not happen either.
In the end I would like to define the handling of errors as a filter
so that the code does not need to be repeated on every call, but I need to get the error response body, so that the exception can be populated with the correct data.
How can I retrieve the error response in a synchronous WebClient
call?
This question is similar to Spring Webflux : Webclient : Get body on error, which has no accepted answer and some of the suggested approaches use methods that are no deprecated.
Upvotes: 7
Views: 31592
Reputation: 21
I know I'm late here, but I had to do quite some digging the past couple of days to get a solution that I wanted. In our situation, we just wanted the response to process normally from the WebClient's perspective no matter the status. We have a uniform format that all API's adhere to when returning a response between services.
From the ResponseSpec interface JavaDoc for onStatus method:
To suppress the treatment of a status code as an error and process it as a normal response, return Mono.empty() from the function. The response will then propagate downstream to be processed.
var response = webClient
.method()
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> Mono.empty())
.toEntity(String.class)
Can do whatever I want with the response
object from here on.
Upvotes: 2
Reputation: 5924
Here is one approach to handle error responses:
onStatus
to capture error http statusclientResponse.bodyToMono(ErrorResponse.class)
Mono.error(new RuntimeException(error.getMessage()))
. Example uses RuntimeException
but I would suggest to use custom exception to simplify error handling downstream.webClient.get()
.uri("/error/" + httpStatus)
.retrieve()
.onStatus(HttpStatus::isError, clientResponse ->
clientResponse.bodyToMono(ErrorResponse.class)
.flatMap(error ->
Mono.error(new RuntimeException(error.getMessage()))
)
)
.bodyToMono(Response.class)
You don't really need try-catch
. If you block
the above code would return Response
in case of the non-error response and throws exception with custom message in case of error response.
Update
Here is a full test using WireMock
class WebClientErrorHandlingTest {
private WireMockServer wireMockServer;
@BeforeEach
void init() {
wireMockServer = new WireMockServer(wireMockConfig().dynamicPort());
wireMockServer.start();
WireMock.configureFor(wireMockServer.port());
}
@Test
void test() {
stubFor(post("/test")
.willReturn(aResponse()
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withStatus(400)
.withBody("{\"message\":\"Request error\",\"errorCode\":\"10000\"}")
)
);
WebClient webClient = WebClient.create("http://localhost:" + wireMockServer.port());
Mono<Response> request = webClient.post()
.uri("/test")
.retrieve()
.onStatus(HttpStatus::isError, clientResponse ->
clientResponse.bodyToMono(ErrorResponse.class)
.flatMap(error ->
Mono.error(new RuntimeException(error.getMessage() + ": " + error.getErrorCode()))
)
)
.bodyToMono(Response.class);
RuntimeException ex = assertThrows(RuntimeException.class, () -> request.block());
assertEquals("Request error: 10000", ex.getMessage());
}
@Data
private static class ErrorResponse {
private String message;
private int errorCode;
}
@Data
private static class Response {
private String result;
}
}
Upvotes: 6