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