I have a piece of code to send a request via WebClient:
public String getResponse(String requestBody){
...
final WebClient.RequestHeadersSpec<?> request =
client.post().body(BodyInserters.fromValue(requestBody));
final String resp =
req.retrieve().bodyToMono(String.class)
.doOnError(
WebClientResponseException.class,
err -> {
// do something
throw new InvalidRequestException(err.getResponseBodyAsString());
})
.block();
...
}
From the code, it looks like the InvalidRequestException will be thrown when the WebClientResponseException happens. However, the test throws the WebClientResponseException instead of the InvalidRequestException.
Here's the part of unit test I wrote to cover the .doOnError part. I tried the following:
...
when(webClientMock.post()).thenReturn(requestBodyUriMock);
when(requestBodyUriMock.body(any())).thenReturn(requestHeadersMock);
when(requestHeadersMock.retrieve()).thenReturn(responseMock);
when(responseMock.bodyToMono(String.class)).thenThrow(new WebClientResponseException(400, "Bad Request", null, null, null));
try {
String result = someServiceSpy.getResponse(requestBody);
} catch (InvalidRequestException e) {
assertEquals(expectedCode, e.getRawStatusCode());
}
The solution is I need to wrap the exception in Mono.error, and then the doOnError will be triggered.
when(responseMock.bodyToMono(String.class)).thenReturn(Mono.error(thrownException));
Related
I am testing return values in a method but need to also test exceptions.
Below is a code snippet of one of the exceptions - how should I test this ?
#Override
public Response generateResponse(Request request) throws CustomException {
try {
GenerateResponse response = client.generateResponse(headers, generateRequest);
return response;
} catch (FeignException.BadRequest badRequest) {
String message = "Received Bad Request";
throw new CustomException(message, "" + badRequest.status());
} catch (FeignException.Unauthorized unauthorized) {
log.error(unauthorized.contentUTF8());
String message = "Received UnAuthorized Exception ";
throw new CustomException(message, "" + unauthorized.status());
}
}}
I have tested the happy path for the method I am testing using the following:
Mockito.when(service.getResponse(Mockito.any(), Mockito.any())).thenReturn(getResponse);
Mockito.when(service.getResponse(Mockito.any(), Mockito.any())).thenThrow(new CustomException());
If you want the mock to throw an error, you do not want thenReturn but thenThrow
I have a Spring Boot 2.3.1 project, in which I use WebClient to call a remote service.
The remote service is not very reliable and tends to return 500 errors, with and without response bodies. My goal is throw a custom exception that contains the response body (or a default message) so that I can log it, and here's my code :
webClient.get()
.uri(targetServiceUri)
.retrieve()
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(error ->
Mono.error(new MyCustomServiceException(error))
)
)
.toEntity(String.class)
.block();
I have 2 tests using wiremock, the first one works :
#Test
void shouldThrowCustomExceptionWhenServiceReturns500ServerErrorWithNoBody() {
setStubForInValidCheckCall(HttpStatus.INTERNAL_SERVER_ERROR,"{'Error':'invalid request'}");
Throwable thrown =
catchThrowable(() -> myClient.performComplianceCheck(getCompany()));
assertThat(thrown)
.isInstanceOf(MyCustomServiceException.class)
.hasMessageContaining("{'Error':'invalid request'}");
}
private void setStubForInValidCheckCall(HttpStatus httpStatus, String body) {
var response= aResponse().withStatus(httpStatus.value());
if(body!=null){
response=response.withBody(body);
}
stubFor(
get(urlPathMatching("/targetCompliance"))
.willReturn(response));
}
However, the second test in which the response is 500 but there's no body (or if it's an empty string), fails with "java.lang.IllegalStateException: Only one connection receive subscriber allowed.
#Test
void shouldThrowCustomExceptionWhenServiceReturns500ServerErrorWithNoBody() {
setStubForInValidCheckCall(HttpStatus.INTERNAL_SERVER_ERROR,null);
Throwable thrown =
catchThrowable(() -> myClient.performComplianceCheck(getCompany()));
assertThat(thrown)
.isInstanceOf(MyCustomServiceException.class)
.hasMessageContaining("service returned status 500");
}
I am struggling to understand why this happens, and how to fix it..
is it "normal" ? or am I missing something obvious (is it a problem with my test ?) ?
I have found a workaround, but it doesn't feel "webFlux-y" at all, and I still don't understand why the Only one connection receive subscriber allowed was happening :
try {
ResponseEntity<String> responseEntity =
webClient.get()
.uri(targetServiceUri)
.retrieve()
.toEntity(String.class)
.block();
}
catch (WebClientException e) {
if(e instanceof InternalServerError){
var internalServerError=(InternalServerError) e;
if(internalServerError.getStatusCode().is5xxServerError()){
var respBody=internalServerError.getResponseBodyAsString();
if(StringUtils.isEmpty(respBody)){
respBody=MY_STANDARD_MESSAGE +internalServerError.getRawStatusCode() ;
}
throw new MyCustomServiceException(respBody);
}
}
}
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:
In a successful case the response contains a JSON object of type A
In an error case (HTTP status 4xx or 5xx) the response may contain a JSON object of type B
On certain requests with response status 404 I need to return an empty List matching the type of a successful response
In 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.
Here is one approach to handle error responses:
use onStatus to capture error http status
deserialize error response clientResponse.bodyToMono(ErrorResponse.class)
generate new error signal based on the error response 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;
}
}
I have a controller somewhere like this:
#GetMapping(value = "redirect")
public ModelAndView oauthRedirect() {
try {
if (true) {
serviceOAuthMetrics.addRedirectCookieException();
throw new AuthenticationChainException("User or client is null in state token");
}
return new ModelAndView(REDIRECT + redirectUrl + "closewindow.html?connected=true");
} catch (Exception e) {
return new ModelAndView(REDIRECT + redirectUrl + "closewindow.html?connected=false");
}
}
I'm trying to test it like this:
#Test
void oauthRedirectThrowsExceptionUserIdIsNullTest() throws Exception {
RequestBuilder request = MockMvcRequestBuilders
.get("/oauth/redirect")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON);
mockMvc.perform(request)
.andExpect(redirectedUrl("http://localhost:8080/closewindow.html?connected=false"))
//.andExpect(content().string("AuthenticationChainException: User or client is null in state token"))
.andReturn();
}
The tests pass as it asserts the return piece from the catch block. However, I'm not seeing a way to assert which exception was thrown and what is the message inside it? (the line commented out fails the test when uncommented).
Thank you.
You cannot test for the Java exception in the traditional sense. You need to test for the response status, and body if you need to. MockMVC is mocking the HTTP request / response.
I'm using RestTemplate to call my webservice's health actuator endpoint from another webservice of mine to see if the webservice is up. If the webservice is up, all works fine, but when it's down, I get an error 500, "Internal Server Error". If my webservice is down, I'm trying to catch that error to be able to handle it, but the problem I'm having is that I can't seem to be able to catch the error.
I've tried the following and it never enters either of my catch sections
#Service
public class DepositService {
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofMillis(3000))
.setReadTimeout(Duration.ofMillis(3000))
.build();
}
private static void getBankAccountConnectorHealth() {
final String uri = "http://localhost:9996/health";
RestTemplate restTemplate = new RestTemplate();
String result = null;
try {
result = restTemplate.getForObject(uri, String.class);
} catch (HttpClientErrorException exception) {
System.out.println("callToRestService Error :" + exception.getResponseBodyAsString());
} catch (HttpStatusCodeException exception) {
System.out.println( "callToRestService Error :" + exception.getResponseBodyAsString());
}
System.out.println(result);
}
}
I've also tried doing it this way, but same results. It never seems to enter my error handler class.
public class NotFoundException extends RuntimeException {
}
public class RestTemplateResponseErrorHandler implements ResponseErrorHandler {
#Override
public boolean hasError(ClientHttpResponse httpResponse) throws IOException {
return (httpResponse.getStatusCode().series() == CLIENT_ERROR || httpResponse.getStatusCode().series() == SERVER_ERROR);
}
#Override
public void handleError(ClientHttpResponse httpResponse) throws IOException {
if (httpResponse.getStatusCode().series() == HttpStatus.Series.SERVER_ERROR) {
// handle SERVER_ERROR
System.out.println("Server error!");
} else if (httpResponse.getStatusCode().series() == HttpStatus.Series.CLIENT_ERROR) {
// handle CLIENT_ERROR
System.out.println("Client error!");
if (httpResponse.getStatusCode() == HttpStatus.NOT_FOUND) {
throw new NotFoundException();
}
}
}
}
#Service
public class DepositService {
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofMillis(3000))
.setReadTimeout(Duration.ofMillis(3000))
.build();
}
private static void getAccountHealth() {
final String uri = "http://localhost:9996/health";
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new RestTemplateResponseErrorHandler());
String result = null;
result = restTemplate.getForObject(uri, String.class);
System.out.println(result);
}
}
Any suggestions as to how I can call my webservice's health actuator from another webservice and catch if that webservice is down?
It looks like the getForObject doesn't throw either of the exceptions you are catching. From the documentation, it throws RestClientException. The best method I have found for identifying thrown exceptions is to catch Exception in the debugger and inspect it to find out if it's useful.
With the second method, I'm not sure why you would create a bean method for the RestTemplate and then create one with new. You probably should inject your RestTemplate and initialise the ResponseErrorHandler with the RestTemplateBuilder::errorHandler method.
Internal serve error throw HttpServerErrorException You should catch this exception if you want to handle it However the better way to do that is using error handler you can see the following posts to see how to do that:
spring-rest-template-error-handling
spring-boot-resttemplate-error-handling