Catch exceptions with try/catch using Spring's WebClient with blocking/syncronous request
Asked Answered
L

1

12

I need to make a syncronous, blocking request, and I'm using Spring's WebClient instead of Spring's RestTemplate due to the latter being deprecated. I don't need the reactive features in this case, I just want to consume a REST API in a straightforward way without including additional dependencies.

I have the following code, which works as intended:

MyObject object = webClient.get()
    .uri( myUri )
    .retrieve()
    .bodyToMono( MyObject.class )
    .block()

However, I need to manage the cases when I either can't connect to the API, or if I connect but I get a 4xx/5xx code.

So, the straightforward way would be to just put the call inside a try/catch, and catch Spring's WebClientResponseException, which is thrown by .bodyToMono if it gets a 4xx/5xx code:

import org.springframework.web.reactive.function.client.WebClientResponseException;

try {

    MyObject object = webClient.get()
        .uri( myUri )
        .retrieve()
        .bodyToMono( MyObject.class )
        .block()

}

catch ( WebClientResponseException e ) {

    // Logic to handle the exception.

}

This works fine, but doesn't work if the connection is refused (say, if the URL is wrong or if the service is down). In this case, I get the following in my console:

reactor.core.Exceptions$ErrorCallbackNotImplemented: io.netty.channel.AbstractChannel$AnnotatedConnectException: finishConnect(..) failed: Connection refused: /127.0.0.1:8090 Caused by: io.netty.channel.AbstractChannel$AnnotatedConnectException: finishConnect(..) failed: Connection refused: /127.0.0.1:8090 Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): |_ checkpoint ⇢ Request to GET http://127.0.0.1:8090/test [DefaultWebClient] Stack trace: Caused by: java.net.ConnectException: finishConnect(..) failed: Connection refused at io.netty.channel.unix.Errors.throwConnectException(Errors.java:124) ~[netty-transport-native-unix-common-4.1.48.Final.jar:4.1.48.Final] (...)

I'm not sure which exception I need to catch to handle this case.

Besides the above, I also would like to throw a custom exception if the connection is refused and a different custom exception if I get an error code. In the second case, I tried using the .onStatus method:

try {

    MyObject object = webClient.get()
        .uri( myUri )
        .retrieve()
        .onStatus( HttpStatus::is4xxClientError, response -> { 
            return Mono.error( new CustomClientException( "A client error ocurred" ) );
        })
        .bodyToMono( MyObject.class )
        .block()

}

catch ( CustomClientException e ) {

    // Logic to handle the exception.

}

But the exception is not caught inside the catch block, although the stack trace does appear on console.

Is there any way to handle 4xx/5xx codes and connection errors using a try/catch block, hopefully with custom exceptions? Or should I use a different web client and/or change my approach? I'm not familiar with reactive programming.

Thanks in advance.

Lowering answered 17/7, 2020 at 21:15 Comment(9)
You need ConnectException to catch in order to handle this case.Freehand
Why are you using WebClient if you immediately block()?Charade
@Amitkumar, I tried catching ConnectException but I get a compiler error saying that it never gets thrown in the try block.Lowering
@chrylis-cautiouslyoptimistic- Because RestTemplate is deprecated in Spring 5, so the docs recommend using WebClient, which supports both blocking and non-blocking requests. I know I won't get any performance benefit by using block(), but I don't think that's neccesarily wrong in this case, and there's always the chance to refactor the code in the future.Lowering
is there any reason to why you cant place the logic to deal with the exception inside the onStatus block?Ricard
@ThomasAndolf I have a legacy codebase, so I'm trying to make the migration from RestTemplate to WebClient in a way that requires the least amount of refactorization. In several cases, the try blocks not only contain the web request code, but also the logic to manipulate the received data, which can also throw exceptions, so the catch block handles all of the above.Lowering
(Continued) Using onStatus means moving the exception logic inside a method and call it inside both onStatus blocks (for 4xx and 5xx, separatedly), and I still need to keep the original try/catch block to handle the exceptions after the web request, again calling the same method. It's quite the task and doesn't seem very clean, but maybe there's no other way.Lowering
i would consider it bad practice to wrap large blocks of code in a single try catch block. You should handle the exceptions for the WebClient separately, and "other logic" should have their own try catch block to handle their "problems".Ricard
I agree, but in this case the exception logic is the same whether there was a exception with the web client or while doing the other logic. Especifically, the application has to send the exception message to an external service. I could separate each task in their own try/catch block, but the actual exception logic will be the same. Putting said logic inside a method seems to be the best approach to avoid duplicating code, but again, that requires refactorization, which I'm trying to avoid, but maybe I should just bite the bullet.Lowering
B
7

With retrieve(), all exceptions that occur on the underlying HTTP client are wrapped in a RuntimeException called ReactiveException. This is done as a way to bubble up the checked exception through the reactive interface.

A means is provided to get the actual wrapped exception in Exceptions.unwrap(). You can then throw the unwrapped exception, and it can be caught later where appropriate. One way to do this might be the following:

 void makeHttpCall(..) throws Exception {
    try {
        // ..
        // webclient request code
        // ..
    } catch(Exception e) {
        throw Exceptions.unwrap(e);
    }
 }

 // somewhere else:
 try {
     makeHttpCall(..);
 } catch (ConnectException e) {
     // Do your specific error handling
 }

I'm not really fond of declaring a method with throws Exception but at this point it's not at all known what it could be.

Between answered 18/7, 2020 at 17:45 Comment(2)
Apparently unwrap doesn't throw an Exception, but rather a Throwable, so I had to change your example a little bit, but it worked. Like you said, I'm also not fond of throwing generic exceptions, and doing this feels a little hacky, so I think it would be better to just bite the bullet and refactor the code to handle the exceptions the proper reactive way. Still, I'm accepting your answer because it directly answers my question. Many thanks :)Lowering
You won't need to unwrap if your exception is a child of RuntimeException. Please refer to the .onErrorMap section from projectreactor.io/docs/core/snapshot/reference/#which.errorsWeekender

© 2022 - 2024 — McMap. All rights reserved.