Search code examples
spring-webfluxspring-webclientreactor-netty

Spring WebClient : Call method in retry


I have been looking for a solution to the following use case without success, I hope someone can help :

Assuming the following use case. I need to call a customer Api (customerApi) and this api needs a Bearer token which may have expired when I call customerApi. If the token has expired, the customerApi returns a 401 response.

What, I want to do is to retry only once if I received a 401 and call the method to get a new Bearer token. If the retry still returns 401, I need to throw an Exception

The method to get a Bearer token :

private String getToken() {
    return oAuthService.getToken();
}

And the webClient usage to call customerApi (customerWebClient is a bean created with WebClient.Builder) :

public Customer getCustomerById(String customerId, String token) {
        return customerWebClient.get()
            .uri("myurl/customers/{customerId}, customerId)
            .headers(httpHeaders -> {
                httpHeaders.add(HttpHeaders.AUTHORIZATION, "Bearer " + token);
            })
            .retrieve()
            .bodyToMono(Customer.class)
            .onErrorResume(WebClientResponseException.NotFound.class, notFound ->
                        Mono.error(new MyCustomException()))
            .block();
    }

It seems that retryWhen can only be used to upgrade timeout. So I hope that someone know how to achieve this use case ^^

Thanks for your help :)

EDIT :

I tried to use retryWhen(Retry.onlyIf(...)) from reactor-extra but the good old retryWhen from this package is now deprecated (solution based on : Adding a retry all requests of WebClient)


Solution

  • The method

    public final Mono<T> retryWhen(Function<Flux<Throwable>, ? extends Publisher<?>> whenFactory)
    

    has been deprecated and now the preferred method is

    public final Mono<T> retryWhen(Retry retrySpec)
    

    So, you can modify your code to something like this to make it work with the new retryWhen

    public Customer getCustomerById(String customerId, String token) {
    
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + token);
    
        final RetrySpec retrySpec = Retry.max(1).doBeforeRetry(
            retrySignal -> headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + someTokenGetterMethod()))
            .filter(throwable -> throwable.getClass() == Unauthorized.class);
    
        return Mono.defer(() -> webClient.get().uri("myurl/customers/{customerId}, customerId")
            .headers(httpHeaders -> httpHeaders.addAll(headers))
            .retrieve()
            .bodyToMono(Customer.class))
            .retryWhen(retrySpec)
            .onErrorResume(WebClientResponseException.NotFound.class,
                notFound -> Mono.error(new MyCustomException()))
            .block();
    }
    

    Here's a working example using https://httpbin.org/

    public CommandLineRunner commandLineRunner() {
    
        HttpHeaders headers = new HttpHeaders();
    
        final RetrySpec retrySpec = Retry.max(1).doBeforeRetry(
            retrySignal -> headers.add("Authorization", "Bearer 1234")).filter(
            throwable -> throwable.getClass() == Unauthorized.class);
    
        return args -> Mono.defer(() -> webClient.get().uri("https://httpbin.org/bearer")
            .headers(httpHeaders -> httpHeaders.addAll(headers)).retrieve().toEntity(Map.class)
            .retryWhen(retrySpec)
            .subscribe(objectResponseEntity -> System.out
                .println("objectResponseEntity = " + objectResponseEntity.getBody()));
    }
    

    Also, I do not think the way you're trying to manipulate headers in retry to add authorization token is the right way of achieving this. You must come up with a better solution/design.