Spring WebClient - How to handle error scenarios - java

We're using org.springframework.web.reactive.function.client.WebClient with
reactor.netty.http.client.HttpClient as part of Spring 5.1.9 to make requests using the exchange() method. The documentation for this method highlights the following:
... when using exchange(), it is the responsibility of the application
to consume any response content regardless of the scenario (success,
error, unexpected data, etc). Not doing so can cause a memory leak.
Our use of exchange() is rather basic, but the documentation for error scenarios is unclear to me and I want to be certain that we are correctly releasing resources for all outcomes. In essence, we have a blocking implementation which makes a request and returns the ResponseEntity regardless of the response code:
try {
...
ClientResponse resp = client.method(method).uri(uri).syncBody(body).exchange().block();
ResponseEntity<String> entity = resp.toEntity(String.class).block();
return entity;
} catch (Exception e) {
// log error details, return internal server error
}
If I understand the implementation, exchange() will always give us a response if the request was successfully dispatched, regardless of response code (e.g. 4xx, 5xx). In that scenario, we just need to invoke toEntity() to consume the response. My concern is for error scenarios (e.g. no response, low-level connection errors, etc). Will the above exception handling catch all other scenarios and will any of them have a response that needs to be consumed?
Note: ClientResponse.releaseBody() was only introduced in 5.2

The response have to be consumed when the request was made, but if you can't do the request probably an exception was be throwed before, and you will no have problems with response.
In the documentation says:
NOTE: When using a ClientResponse through the WebClient exchange() method, you have to make sure that the body is consumed or released by using one of the following methods:
body(BodyExtractor)
bodyToMono(Class) or bodyToMono(ParameterizedTypeReference)
bodyToFlux(Class) or bodyToFlux(ParameterizedTypeReference)
toEntity(Class) or toEntity(ParameterizedTypeReference)
toEntityList(Class) or toEntityList(ParameterizedTypeReference)
toBodilessEntity()
releaseBody()
You can also use bodyToMono(Void.class) if no response content is expected. However keep in mind the connection will be closed, instead of being placed back in the pool, if any content does arrive. This is in contrast to releaseBody() which does consume the full body and releases any content received.
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/reactive/function/client/ClientResponse.html
You can try to use .retrieve() instead .exchange() and handle errors as your preference.
public Mono<String> someMethod() {
return webClient.method(method)
.uri(uri)
.retrieve()
.onStatus(
(HttpStatus::isError), // or the code that you want
(it -> handleError(it.statusCode().getReasonPhrase())) //handling error request
)
.bodyToMono(String.class);
}
private Mono<? extends Throwable> handleError(String message) {
log.error(message);
return Mono.error(Exception::new);
}
In this example I used Exception but you can create some exception more specific and then use some exception handler to return the http status that you want.
Is not recommended to use block, a better way is pass the stream forward.

create some exception classes
Autowired ObjectMapper
Create a method that returns Throwable
Create a custom class for Error.
return webClient
.get()
.uri(endpoint)
.retrieve()
.bodyToMono(Model.class)
.onErrorMap(WebClientException.class, this::handleHttpClientException);
private Throwable handleHttpClientException(Throwable ex) {
if (!(ex instanceof WebClientResponseException)) {
LOG.warn("Got an unexpected error: {}, will rethrow it", ex.toString());
return ex;
}
WebClientResponseException wcre = (WebClientResponseException)ex;
switch (wcre.getStatusCode()) {
case NOT_FOUND -> throw new NotFoundException(getErrorMessage(wcre));
case BAD_REQUEST -> throw new BadRequestException(getErrorMessage(wcre));
default -> {
LOG.warn("Got a unexpected HTTP error: {}, will rethrow it", wcre.getStatusCode());
LOG.warn("Error body: {}", wcre.getResponseBodyAsString());
return ex;
}
}
}
private String getErrorMessage(WebClientResponseException ex) {
try {
return mapper.readValue(ex.getResponseBodyAsString(), HttpErrorInfo.class).getMessage();
} catch (IOException ioe) {
return ex.getMessage();
}
}

Related

MuleSoft - mock httpClient.send for extension unit tests

I have created a custom extension (Connector), which sends an HttpRequest (using org.mule.runtime.http.api.client.HttpClient and the related classes).
The extension's unit tests file contains the following test, to which I've added a simple Mockito mock to throw a TimeoutException when the HTTP request is being sent:
public class DemoOperationsTestCase extends MuleArtifactFunctionalTestCase {
/**
* Specifies the mule config xml with the flows that are going to be executed in the tests, this file lives in the test resources.
*/
#Override
protected String getConfigFile() {
return "test-mule-config.xml";
}
#Test
public void executeSayHiOperation() throws Exception {
HttpClient httpClient = mock(HttpClient.class);
HttpRequest httpRequest = mock(HttpRequest.class);
when(httpClient.send(any(HttpRequest.class), anyInt(), anyBoolean(), any(HttpAuthentication.class))).thenThrow(new TimeoutException());
String payloadValue = ((String) flowRunner("sayHiFlow").run()
.getMessage()
.getPayload()
.getValue());
assertThat(payloadValue, is("Hello Mariano Gonzalez!!!"));
}
}
The test should fail because the function should throw a TimeoutException, it is what I want for now.
The code that is being tested is as follows (redacted for convenience):
HttpClient client = connection.getHttpClient();
HttpResponse httpResponse = null;
String response = "N/A";
HttpRequestBuilder builder = HttpRequest.builder();
try {
httpResponse = client
.send(builder
.addHeader("Authorization", authorization)
.method("POST")
.entity(new ByteArrayHttpEntity("Hello from Mule Connector!".getBytes()))
.uri(destinationUrl)
.build(),
0, false, null);
response = IOUtils.toString(httpResponse.getEntity().getContent());
} catch (IOException e) {
e.printStackTrace();
throw new ModuleException(DemoError.NO_RESPONSE, new Exception("Failed to get response"));
} catch (TimeoutException e) {
e.printStackTrace();
throw new ModuleException(DemoError.NO_RESPONSE, new Exception("Connection timed out"));
}
But I always get the "Failed to get response" error message, which is what I get when I run the Connector with a nonexistent server, therefore the mock isn't working (it actually tries to send an HTTP request).
I am new to Java unit testing, so it might be a general mocking issue and not specific to MuleSoft - though I came across other questions (such as this one and this one), I tried the suggestions in the answers and the comments, but I get the same error. I even tried to use thenReturn instead of thenThrow, and I get the same error - so the mock isn't working.
Any idea why this is happening?

Java with Spring Boot: Throw an exception that returns a specific HTTP code to the client

I am developing a web application using Java and Spring Boot.
Specifically, I am reviewing some code written by other developers. I use Postman to make HTTP calls to my application.
There are cases where my application needs to inform the caller of certain situations. In this example, the developer of the application threw a JwtTokenException to the caller in case of IOException.
try {
myToken = methodThatObtainsAToken(tokenInput);
}catch(IOException ex) {
throw new JwtTokenException("Error 401", JwtStatusResponse.JWT_NOT_VALID); // TODO: cambiare
}
When something goes wrong here is what happens on POSTMAN:
I have so many other very similar situations in this code and I have to do the following thing: I have to replace the code throw new JwtTokenException so that it throws exceptions that make the caller understand (so I can see them with Postman) that an HTTP error has occurred (with some code).
In the example I wrote I wrote "Error 401" only as a string. There are other places in the code where I use "Error 500", "Error 302" and so on.. But these are just information strings, and do not correspond to the real error thrown to the client. How do I throw correct exceptions that raise "HTTP errors"?
One usually returns response entity with body object and status set to provide
interesting information:
return ResponseEntity.ok()
.headers(HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, trackDTO.getId().toString()))
.body(result);
Response Entity is org.springframework.http.ResponseEntotuy which provides a lot of convenience methods to setup all the headers and whatever is necessary.
If most of the exceptions are handled like in code you have posted and you don't want to change return types of methods, just replace them with
catch(Exception e){
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
which will give the following output
{
"timestamp": "2021-03-10T09:25:04.823+0000",
"status": 404,
"error": "Not Found",
"message": "Not Found",
"path": "/account"
}
if you'd like to get exception info just add
catch(Exception e){
throw new ResponseStatusException(HttpStatus.NOT_FOUND,null, e);
}
response:
{
...
"message": "404 NOT_FOUND; nested exception is java.lang.ArithmeticException",
...
}
note that it will be logged if you didn't declare your own exception resolver
2021-03-10 15:28:20.043 WARN 11392 --- [nio-8090-exec-2] .w.s.m.a.ResponseStatusExceptionResolver : Resolved [org.springframework.web.server.ResponseStatusException: 404 NOT_FOUND "Not Found"; nested exception is java.lang.ArithmeticException]
Instead of null you could pass a string describing exception, which will replace "message" in json response.
From your attached screenshot. Your application returned 401 Unauthorized. I think you no longer need to pass the error code since you get it from the response itself. Exceptions do not return Http Status Code and you as a developer is responsible to telling your app which error code to return. For example in your RestController
#GetMapping(path = "{id}")
public ResponseEntity<ResponseModel> getEmployee(#PathVariable("id") String id) {
ResponseModel result = employeeService.findById(id);
return new ResponseEntity<>(result, HttpStatus.OK); // <--- THIS
}
And in your custom exception handler, you can do it like this.
NOTE: This triggers when you call from your Jwt Verifier Filter
#ExceptionHandler(value = ExpiredJwtException.class)
public ResponseEntity<Object> handleExpiredJwt(ExpiredJwtException e) {
ApiException apiException = new ApiException(e.getLocalizedMessage(), HttpStatus.UNAUTHORIZED);
return new ResponseEntity<>(apiException, UNAUTHORIZED);
}

How do I log out the body of a failed response to a Spring WebFlux WebClient request while returning the response to the caller?

I'm very new to reactive programming and I have a REST service that takes a request and then calls to another API using the WebFlux WebClient. When the API responds with a 4xx or 5xx response, I want to log the response body in my service, and then pass on the response to the caller. I've found a number of ways to handle logging the response, but they generally return Mono.error to the caller, which is not what I want to do. I have this almost working, but when I make the request to my service, while I get back the 4xx code that the API returned, my client just hangs waiting for the body of the response, and the service never seems to complete processing the stream. I'm using Spring Boot version 2.2.4.RELEASE.
Here's what I've got:
Controller:
#PostMapping(path = "create-order")
public Mono<ResponseEntity<OrderResponse>> createOrder(#Valid #RequestBody CreateOrderRequest createOrderRequest) {
return orderService.createOrder(createOrderRequest);
}
Service:
public Mono<ResponseEntity<OrderResponse>> createOrder(CreateOrderRequest createOrderRequest) {
return this.webClient
.mutate()
.filter(OrderService.errorHandlingFilter(ORDERS_URI, createOrderRequest))
.build()
.post()
.uri(ORDERS_URI)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(createOrderRequest)
.exchange()
.flatMap(response -> response.toEntity(OrderResponse.class));
}
public static ExchangeFilterFunction errorHandlingFilter(String uri, CreateOrderRequest request) {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
if (clientResponse.statusCode() != null && (clientResponse.statusCode().is5xxServerError() || clientResponse.statusCode().is4xxClientError())) {
return clientResponse.bodyToMono(String.class)
.flatMap(errorBody -> OrderService.logResponseError(clientResponse, uri, request, errorBody));
} else {
return Mono.just(clientResponse);
}
});
}
static Mono<ClientResponse> logResponseError(ClientResponse response, String attemptedUri, CreateOrderRequest orderRequest, String responseBody) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
try {
log.error("Response code {} received when attempting to hit {}, request:{}, response:{}",
response.rawStatusCode(), attemptedUri, objectMapper.writeValueAsString(orderRequest),
responseBody);
} catch (JsonProcessingException e) {
log.error("Error attempting to serialize request object when reporting on error for request to {}, with code:{} and response:{}",
attemptedUri, response.rawStatusCode(), responseBody);
}
return Mono.just(response);
}
As you can see, I'm simply trying to return a Mono of the original response from the logResponseError method. For my testing, I'm submitting a body with a bad element which results in a 422 Unprocessable Entity response from the ORDERS_URI endpoint in the API I'm calling. But for some reason, while the client that called the create-order endpoint receives the 422, it never receives the body. If I change the return in the logResponseError method to be
return Mono.error(new Exception("Some error"));
I receive a 500 at the client, and the request completes. If anyone knows why it won't complete when I try to send back the response itself, I would love to know what I'm doing wrong.
Can't have your cake and eat it too!
The issue here is that you are trying to consume the body of the response twice, which is not allowed. Normally you would get an error for doing so.
Once in
return clientResponse.bodyToMono(String.class)
but also in
response.toEntity(OrderResponse.class)
which actually runs
#Override
public <T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType) {
return WebClientUtils.toEntity(this, bodyToMono(bodyType));
}
So one solution would be to process the ResponseEntity instead of the ClientResponse as follows since you don't actually want to do any reactive stuff with the body
public Mono<ResponseEntity<OrderResponse>> createOrder(CreateOrderRequest createOrderRequest) {
return this.webClient
//no need for mutate unless you already have things specified in
//base webclient?
.post()
.uri(ORDERS_URI)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(createOrderRequest)
.exchange()
//Here you map the response to an entity first
.flatMap(response -> response.toEntity(OrderResponse.class))
//Then run the errorHandler to do whatever
//Use doOnNext since there isn't any reason to return anything
.doOnNext(response ->
errorHandler(ORDERS_URI,createOrderRequest,response));
}
//Void doesn't need to return
public static void errorHandler(String uri, CreateOrderRequest request,ResponseEntity<?> response) {
if( response.getStatusCode().is5xxServerError()
|| response.getStatusCode().is4xxClientError())
//run log method if 500 or 400
OrderService.logResponseError(response, uri, request);
}
//No need for redundant final param as already in response
static void logResponseError(ResponseEntity<?> response, String attemptedUri, CreateOrderRequest orderRequest) {
//Do the log stuff
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
try {
log.error("Response code {} received when attempting to hit {}, request:{}, response:{}",
response.getStatusCodeValue(), attemptedUri, objectMapper.writeValueAsString(orderRequest),
response.getBody());
} catch (JsonProcessingException e) {
log.error("Error attempting to serialize request object when reporting on error for request to {}, with code:{} and response:{}",
attemptedUri, response.getStatusCodeValue(), response.getBody());
}
}
Note that there isn't really a reason to use the ExchangeFilter since you aren't actually doing any filtering, just performing an action based off the response

How to handle exception in spring with #ControllerAdvice even though we are handling exception in catch block

Not able to use #ControllerAdvice and #AfterThrowing when method surrounded by try and catch block.
I can explain step by step
Step 1: In my spring application all handlers(methods) are handle exception by try and catch block.
Step 2: So requirement i need to trigger a email when the exception occurs in all handlers methods. But my application having 100's of methods. So try with #ControllerAdvice to handle the exceptions by using #ExceptionHandler annotation. I know that it wont work because we already handling our exception in catch block. So it cant look at #ControllerAdvice.
Step 3: I try with Aop #AfterThrowing advice also. It is not working. So i cant able to remove all catch blocks in entire application code. It is very difficultly to do that.
But my question is that
Is there any way in spring to handle the exception even we are handling it.
We returning back status code like 400 . In Spring they any Advice to identify status code.
Because are retiring ResponseEntity Status back as response.
#RequestMapping(value = "/service/getDetails", method = RequestMethod.POST, produces = { "application/json" })
public ResponseEntity<Map<String, Object>> getDetails(#RequestBody Details details, HttpServletRequest request) {
ResponseEntity<Map<String, Object>> response = null;
try {
/// Some code
} catch (Exception e) {
logger.error("Exception while DetailsController: " + e.getMessage());
response = new ResponseEntity<>(HttpStatus.BAD_REQUEST);
/*
* int status=response.getStatusCodeValue();
* scheduledMailTrigerService.sendErrorLogInfo(request,e,status);
*/
}
return response;
}
#ControllerAdvice
public class AppExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(value = { Exception.class })
public String handleAnyException(Exception ex, WebRequest request) {
System.out.println("Done");
return "Done";
}
}
You can use an #Around advice to capture the result of the method execution... my suggestion is that you remove the try catch from all your endpoints, just write a try catch wrapping the ProceedingJoinPoint proceed() execution. This way you can return a ResponseEntity 400 when an exception occurs, alongside execututing email sending logic.

How to handle error responses in a chain of CompletableFutures?

I have a long chain of completable futures in my project, with each step calling a backend API, which can give multiple error responses and one success response. Now, after parsing the response, I need to judge if it's an error, then I need to show to the user. I also need to know which stage in my chain, produced this error.
My approach right now (shown below) is to throw a Runtime Exception whenever I encounter an error response, and then append exceptionally block to my chain. I feel that this is not the best way to do it, since a runtime exception doesn't fit in this scenario. It also makes my code ugly, since I have to do it whenever I process a response, leading to an extra exception check. Is there a better way to do it?
CompletableFuture.supplyAsync(() -> {
//some api call
Response response = request.send();
if(response.hasError()){ //this is what I am doing right now
logger.error("this is error response");
throw new ResponseErrorException("Error response received for request");
}
})
This is basically repeated for every step in the chain.
Summary: If I get a failure response in any of the steps in a CompletableFuture chain, what's a good way to propagate it to the user?
Edit: If there's no better approach, please feel free to share your views on my approach.
My suggestion is using Decorator pattern for the responses. Suggest you have something like this
CompletableFuture
.supplyAsync(() -> {
//some api call
Response response = request.send();
if(response.hasError()){ //this is what I am doing right now
throw new ResponseErrorException("Error response received for request");
}
})
.thenApply(() -> {
//some api call
Response response = request.send();
if(response.hasError()){ //this is what I am doing right now
throw new ResponseErrorException("Another Error response received for request");
}
})
.exceptionally(ex -> "Error: " + ex.getMessage());
and if you would like to avoid duplication in throwing exceptions you could use following approach
CompletableFuture
.supplyAsync(() -> {
//some api call
Response response = ThrowExceptionOnErrorResponse(request.send());
})
.thenApply(() -> {
//some api call
Response response = ThrowExceptionOnErrorResponse(request.send());
}
})
.exceptionally(ex -> "Error: " + ex.getMessage());
class ThrowExceptionOnError implements Response {
Response originalResponse;
ThrowExceptionOnError(Response originalResp) {
if(response.hasError()) {
throw new ResponseErrorException("Another Error response received for request");
}
this.originalResponse = originalResponse;
}

Categories