Handling errors from Spring WebClient in another method
Asked Answered
S

2

6

In a Spring Boot application, I'm using WebClient to invoke a POST request to a remote application. The method currently looks like this:

// Class A
public void sendNotification(String notification) {
    final WebClient webClient = WebClient.builder()
            .defaultHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
            .build();
    webClient.post()
            .uri("http://localhost:9000/api")
            .body(BodyInserters.fromValue(notification))
            .retrieve()
            .onStatus(HttpStatus::isError, clientResponse -> Mono.error(NotificationException::new))
            .toBodilessEntity()
            .block();
    log.info("Notification delivered successfully");
}

// Class B
public void someOtherMethod() {
    sendNotification("test");
}

The use case is: A method in another class calls sendNotification and should handle any error, i.e. any non 2xx status or if the request couldn't even be sent.

But I'm struggling with the concept of handling errors in the WebClient. As far as I understood, the following line would catch any HTTP status other than 2xx/3xx and then return a Mono.error with the NotificationException (a custom exception extending Exception).

onStatus(HttpStatus::isError, clientResponse -> Mono.error(NotificationException::new))

But how could someOtherMethod() handle this error scenario? How could it process this Mono.error? Or how does it actually catch the NotificationException if sendNotification doesn't even throw it in the signature?

Singleton answered 19/1, 2022 at 21:29 Comment(0)
A
13

Well, there are many ways to handle errors, it really depends on what you want to do in case of an error.

In your current setup, the solution is straightforward: first, NotificationException should extend RuntimeException, thus, in case of an HTTP error, .block() will throw a NotificationException. It is a good practice to add it in the signature of the method, accompanied with a Javadoc entry.
In another method, you just need to catch the exception and do what you want with it.

/**
 * @param notification
 * @throws NotificationException in case of a HTTP error
 */
public void sendNotification(String notification) throws NotificationException {
    final WebClient webClient = WebClient.builder()
        .defaultHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
        .build();
    webClient.post()
        .uri("http://localhost:9000/api")
        .body(BodyInserters.fromValue(notification))
        .retrieve()
        .onStatus(HttpStatus::isError, clientResponse -> Mono.error(NotificationException::new))
        .toBodilessEntity()
        .block();
    log.info("Notification delivered successfully");
}

public void someOtherMethod() {
    try {
        sendNotification("test");
    } catch (NotificationException e) {
        // Treat exception
    }
}

In a more reactive style, you could return a Mono and use onErrorResume().

public Mono<Void> sendNotification(String notification) {
    final WebClient webClient = WebClient.builder()
        .defaultHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
        .build();
    return webClient.post()
        .uri("http://localhost:9000/api")
        .body(BodyInserters.fromValue(notification))
        .retrieve()
        .onStatus(HttpStatus::isError, clientResponse -> Mono.error(NotificationException::new))
        .bodyToMono(Void.class);
}

public void someOtherMethod() {
    sendNotification("test")
        .onErrorResume(NotificationException.class, ex -> {
            log.error(ex.getMessage());
            return Mono.empty();
        })
        .doOnSuccess(unused -> log.info("Notification delivered successfully"))
        .block();
}
Alfaro answered 19/1, 2022 at 23:21 Comment(2)
Are not both solutions non-reactive? we are using block() in both of them.Overbearing
@MuhammadAbuBakr Well, that depends on the point of view. The key difference is that in the second case, the sendNotification method is non-blocking, which starts the path of reactive programming. Moreover, I’m not talking about the underlying mechanics, just programming style: historic Java vs functional programming. In the second example, you could delete the block, return a Mono and you’d be non-blocking all the way, except at some point you still need to get the result (by blocking or making a non-blocking endpoint).Alfaro
T
4

Using imperative/blocking style you can surround it with a try-catch:

try {
    webClient.post()
            .uri("http://localhost:9000/api")
            .body(BodyInserters.fromValue(notification))
            .retrieve()
            .onStatus(HttpStatus::isError, clientResponse -> Mono.error(NotificationException::new))
            .toBodilessEntity()
            .block();
} catch(NotificationException e) {...}

A reactive solution would be to use the onErrorResume operator like this:

    webClient.post()
            .uri("http://localhost:9000/api")
            .body(BodyInserters.fromValue(notification))
            .retrieve()
            .onErrorResume(e -> someOtherMethod())
            .toBodilessEntity();

Here, the reactive method someOtherMethod() will be executed in case of any error.

Tongs answered 19/1, 2022 at 22:56 Comment(3)
Using .block in reactive scenarios is not a good idea.Gammer
@Gammer please read my answer carefully. Two solutions are provided, The second is the reactive one. WebClient can also be used on imperative/blocking applications.Tongs
I see that you deleted the .block from the second example, now it looks better :)Gammer

© 2022 - 2024 — McMap. All rights reserved.