Spring reactor - How to properly throw Exception? - java

I have this code:
public void createImage(Image image) {
tokenProvider.getAccessToken()
.flatMap(accessToken -> restCllent.decodeColour(url, accessToken.getToken())
.flatMap(colour -> restClient.createImage(url, accessToken.getToken())))
.subscribe();
}
in the function decodeColour I have this code which call external service:
public Mono<Colour> decodeColour(String path, String token) {
log.info("Executing GET request to {}", path);
return webClient
.get()
.uri(path)
.header(HttpHeaders.AUTHORIZATION, String.format(TOKEN_BEARER, token))
.retrieve()
.bodyToMono(Colour.class)
.onErrorResume(e -> Mono.error(new RuntimeException("Error occurred during colour decoding: " + e.getMessage())));
}
When external service returns for example 401, I handled that in onErrorResume(),
and I want to just throw RuntimeException.
In the code above "createImage(Image image)" function, how I can force the code to just throw the RuntimeException if the error occurred in decodeColour()?
If I leave it like this, I get
ErrorCallbackNotImplemented: RuntimeException: Error occurred during colour decoding.
And if I add some error callback like doOnError, it just handle the error, but I want to program throws my RuntimeException. It is important to me to do that.

Related

CompletableFuture - skip errors and take only validate data

I have a specific code which is working fine if webclient response is OK. If any error, then the get() method throwing error and the thread blocked forever.
#SneakyThrows
public List<ResponseData> validateExpression(List<RequestData> RequestDataList, Data data) {
System.out.println(Instant.now());
final List<Mono<ResponseData>> monoList = new ArrayList<>();
RequestDataList.parallelStream().forEach(requestData -> {
try {
ObjectMapper mapper = new ObjectMapper();
log.info("Diversity API request data:");
log.info(mapper.writeValueAsString(requestData));
Mono<ResponseData> monoResponse = webClient
.post()
.uri("http://...")
.contentType(MediaType.APPLICATION_JSON)
.header(API_KEY_HEADER, config.getApiKey())
.body(Mono.just(requestData), RequestData.class)
.retrieve()
.bodyToMono(ResponseData.class);
System.out.println("create mono response lazy initialization");
monoList.add(monoResponse);
} catch (Exception e) {
log.info(e.getMessage());
}
});
System.out.println(Instant.now());
CompletableFuture<List<ResponseData>> futureCount = new CompletableFuture<>();
List<ResponseData> responseDataList = new ArrayList<>();
Mono.zip(monoList, Arrays::asList)
.flatMapIterable(objects -> objects)
.doOnComplete(() -> {
futureCount.complete(responseDataList);
}).subscribe(responseData -> {
responseDataList.add((ResponseData) responseData);
});
return futureCount.get();
}
It is working fine with successful case. If there is any error from the webclient it is throwing error and thread blocked forever.
How to skip the errors and get only validate response data ?
How to avoid deadLock on this case?
You should look at CompletableFuture::get method's doc:
It throws three different checked exceptions. Lombok's #SneakyThrows annotation hides them so they aren't managed by your method. You should probably add a try/catch block to manage these exceptions and skip the errors if you want so.

Spring WebClient - How to handle error scenarios

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();
}
}

WebClient reactor.core.Exceptions$ErrorCallbackNotImplemented

So I have the following piece of code to do a GET to a remote machine:
webClient.get()
.uri(myUri)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
.subscribe(text -> {
LOG.info(text);
});
I get this exception, no problem, I'm expecting it, but it's really hard to find any documentation how to handle these errors:
reactor.core.Exceptions$ErrorCallbackNotImplemented: java.net.UnknownHostException
To handle these exceptions you need to add the following, adapt it to your case (in my case if I get an unkownHostException I simply log a warning that the requested service is not present:
webClient.get()
.uri(myUri)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> {
if (e instanceof UnknownHostException) {
LOG.warn("Failed to get myStuff, desired service not present");
} else {
LOG.error("Failed to get myStuff");
}
return Mono.just("Encountered an exception");
})
.subscribe(text -> {
LOG.info(text);
});
You handle the error, and send something to the next step. I really wish there was a way to stop there and not pass anything down the pipe.

How can I catch an exception thrown in the onStatus method of a blocking webclient GET request?

I am using a WebClient right now instead of rest template to call an API. The goal is that in a near-future sprint we would make all of our clients reactive and non-blocking, but in the short term we can use a blocking call but at least have the WebClient in place. One thing we want to do is throw a custom exception when a 204 No Content status is returned, so that the code using the client must catch the exception
I have tried throwing exceptions from the .onStatus(...), but what happens is that it doesn't throw the PersonNotFoundException from the code below, it throws a reactor.core.Exceptions$ReactiveException with the PersonNotFoundException nested
Client code
public PersonDto getPersonDetails(String lastName) throws PersonNotFoundException{
return webClient.get()
.uri(personEndpoint + "/{lastName}", lastName)
.retrieve()
.onStatus(status -> status.equals(HttpStatus.NO_CONTENT),
clientResponse -> Mono.error(new PersonNotFoundException("Person " + lastName + "Not Found")))
.bodyToMono(PersonDto.class)
.block();
}
Calling Code
PersonDto personDto = null;
try {
personDto = personServiceCLient.getPersonDetails("Smith");
} catch (PersonNotFoundException e) {
//do custom logic for 204 errors
}
The result I would hope for is that the PersonNotFoundException would be caught in the catch statement of the calling code. The result is that a ReactiveException is thrown, and the uncaught exception terminates my program:
reactor.core.Exceptions$ReactiveException: com.sample.demo.PersonNotFoundException: Person Smith Not Found
at reactor.core.Exceptions.propagate(Exceptions.java:326) ~[reactor-core-3.2.6.RELEASE.jar:3.2.6.RELEASE]
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:91) ~[reactor-core-3.2.6.RELEASE.jar:3.2.6.RELEASE]
at reactor.core.publisher.Mono.block(Mono.java:1494) ~[reactor-core-3.2.6.RELEASE.jar:3.2.6.RELEASE]
at com.sample.demo.client.PersonServiceClient.getPersonDetails(PersonServiceClient.java.java:45) ~[classes/:na]
at com.sample.demo.PersonServiceImpl.addPersonToHousehold(PersonServiceImpl.java:120) ~[classes/:na]
your custom exception PersonNotFoundException gets wrapped into reactor.core.Exceptions$ReactiveException. To avoid such situations always use RuntimeException. Now you have to extend your custom exception class from RuntimeException.
For Example:
public class PersonNotFoundException extends RuntimeException
Try using the Exceptions util class. Might be too late now but oh well.
public PersonDto getPersonDetails(String lastName) throws PersonNotFoundException{
return webClient.get()
.uri(personEndpoint + "/{lastName}", lastName)
.retrieve()
.onStatus(status -> status.equals(HttpStatus.NO_CONTENT),
clientResponse -> throw Exceptions.propagate(new PersonNotFoundException("Person " + lastName + "Not Found")))
.bodyToMono(PersonDto.class)
.block();
}

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