3

I am using WebClient from Spring WebFlux to communicate with a REST API backend from a Spring client.

When this REST API backend throws an exception, it answers with a specific format (ErrorDTO) that I would like to collect from my client.

What I have tried to do is to make my client throw a GestionUtilisateurErrorException(ErreurDTO) containing this body once the server answers with a 5xx HTTP status code.

I have tried several options :

I/ onStatus

@Autowired
WebClient gestionUtilisateursRestClient;

gestionUtilisateursRestClient
    .post()
    .uri(profilUri)
    .body(Mono.just(utilisateur), UtilisateurDTO.class)
    .retrieve()
    .onStatus(HttpStatus::is5xxServerError,
        response -> {
            ErreurDTO erreur = response.bodyToMono(ErreurDTO.class).block();
            
            return Mono.error(new GestionUtilisateursErrorException(erreur));
        }
    )   
    .bodyToMono(Void.class)
    .timeout(Duration.ofMillis(5000))         
    .block();

This method doesn't work because webclient doesn't allow me to call the block method in the onStatus. I am only able to get a Mono object and I can't go further from here.

It seems like "onStatus" method can't be used in a WebClient blocking method, which means I can throw a custom Exception, but I can't populate it with the data from the response body.

II/ ExchangeFilterFunction

@Bean
WebClient gestionUtilisateursRestClient() {
    return WebClient.builder()
      .baseUrl(gestionUtilisateursApiUrl)
      .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
      .filter(ExchangeFilterFunction.ofResponseProcessor(this::gestionUtilisateursExceptionFilter))
      .build();
}   

private Mono<ClientResponse> gestionUtilisateursExceptionFilter(ClientResponse clientResponse) {
    if(clientResponse.statusCode().isError()){
        return clientResponse.bodyToMono(ErreurDTO.class)
            .flatMap(erreurDto -> Mono.error(new GestionUtilisateursErrorException(
                erreurDto
            )));
    }
    
    return Mono.just(clientResponse);
}

This method works but throw a reactor.core.Exceptions$ReactiveException that I am struggling to catch properly (reactor.core.Exceptions is not catchable, and ReactiveException is private).

This Exception contains in its Cause the exception I need to catch (GestionUtilisateurErrorException) but I need a way to catch it properly.

I also tried to use "onErrorMap" and "onErrorResume" methods but none of them worked the way I needed.

Edit 1 : I am now using the following workaround even if I feel it's a dirty way to do what I need :

gestionUtilisateursRestClient
            .post()
            .uri(profilUri)
            .body(Mono.just(utilisateur), UtilisateurDTO.class)
            .retrieve()
            .onStatus(h -> h.is5xxServerError(),
              response -> {
                    return response.bodyToMono(ErreurDTO.class).handle((erreur, handler) -> {                         
                      LOGGER.error(erreur.getMessage());
                      handler.error(new GestionUtilisateursErrorException(erreur));
                    });
                  }
                )
            .bodyToMono(String.class)
            .timeout(Duration.ofMillis(5000))
            
            .block();
    }
    catch(Exception e) {
        LOGGER.debug("Erreur lors de l'appel vers l'API GestionUtilisateur (...)");
        if(ExceptionUtils.getRootCause(e) instanceof GestionUtilisateursErrorException) {
            throw((GestionUtilisateursErrorException) e.getCause());
        }
        else {
            throw e;
        }
    }

Here, it throws the expected GestionUtilisateursErrorException that I can handle synchronously.

I might implement this in a global handler to avoid writing this code around each call to my API.

Thank you. Kevin

kevinls
  • 31
  • 1
  • 5

2 Answers2

0

I've encountered a similar case for accessing the response body that might be of use to you using the Mono.handle() method (see https://projectreactor.io/docs/core/release/api/index.html?reactor/core/publisher/Mono.html).

Here handler is a SynchronousSink (see https://projectreactor.io/docs/core/release/api/reactor/core/publisher/SynchronousSink.html) and can call at most next(T) one time, and either complete() or error().

In this case, I call 'handler.error()' with a new GestionUtilisateursErrorException constructed with the 'erreur'.

.onStatus(h -> h.is5xxServerError(),
  response -> {
    return response.bodyToMono(ErreurDTO.class).handle((erreur, handler) -> {
      // Do something with erreur e.g.
      log.error(erreur.getErrorMessage());
      // Call handler.next() and either handler.error() or handler.complete()
      handler.error(new GestionUtilisateursErrorException(erreur));
    });
  }
)
Dharman
  • 30,962
  • 25
  • 85
  • 135
Finbarr O'B
  • 1,435
  • 1
  • 15
  • 18
  • I tried to implement this solution. I can actually log what the server responded with thanks to the "log.error" operation. But it still throws a reactor.core.Exceptions$ReactiveException. I have found a workaround that I specified as an edit or my original question, but I feel like it is a dirty way to do it. – kevinls May 28 '21 at 07:58
0

I had the same problem. It seems that checked exceptions are not allowed to be thrown.

Look at this document found here: https://www.jvt.me/posts/2022/03/04/spring-webflux-exceptions/

There are two solution options:

1 - In the test check the cause like this:

assertThatThrownBy(class.methodCall()).hasCauseInstanceOf(GestionUtilisateursErrorException.class);

2- Make your exception extend a RuntimeException

Jonathan
  • 127
  • 1
  • 8