spring-bootspring-webfluxproject-reactor

Spring WebClient seems to suppress WebClientResponseException using Retry.withThrowable


Using Spring's WebClient I'd like to implement the following behavior.

When the server returns HTTP 429 (Too Many Requests), then I'd like to extract the Retry-After header value. The request shall then be retried after this interval. Here's the current implemenation which works for that case. But I'm experiencing a problem I cannot solve.

If the server responds with, for example, a 500 or any other error status except 429, then the application seems to do or wait for something "indefinitely" but I cannot figure it out. It seems as if the .retryWhen(retryAfterFixedDelay()) swallows/suppresses the WebClientResponseException. When commenting out this line, the exception is thrown.

public SomeObject getSomeObject(int id) {
    return apiClient.get()
            .uri("/some-object/{id}", id)
            .retrieve()
            .onStatus(status -> status.equals(TOO_MANY_REQUESTS), this::throwRateLimitException)
            .bodyToMono(SomeObject.class)
            .retryWhen(retryAfterFixedDelay())
            .block());
}

private static Mono<RateLimitException> throwRateLimitException(ClientResponse response) {
    // extract header value...
    final RateLimitException rateLimitException = new RateLimitException(message, retryAfterSeconds);
    return Mono.error(rateLimitException);
}

private static Retry retryAfterFixedDelay() {
    return Retry.withThrowable(throwableFlux -> throwableFlux
            .filter(RateLimitException.class::isInstance)
            .flatMap(throwable -> {
                RateLimitException rateLimitException = (RateLimitException) throwable;
                log.warn(rateLimitException.getMessage());
                return Mono.delay(ofSeconds(rateLimitException.getRetryAfterSeconds()));
            }));
}

Solution

  • Please try to return either Mono.error(throwable) or e.g. Mono.delay(ofSeconds(5)) for exceptions different from RateLimitException, depending on what you want.

    Something like this

        private static Retry retryAfterFixedDelay() {
            return Retry.withThrowable(throwableFlux -> throwableFlux
                    .flatMap(throwable -> {
                        if (throwable instanceof RateLimitException rateLimitException) {
                            // for RateLimitException, use delay from exception
                            return Mono.delay(ofSeconds(rateLimitException.getRetryAfterSeconds()));
                        } else {
                            return Mono.error(throwable);
                        }
                    }));
        }
    

    or like this if you want all errors to be retried

        private static Retry retryAfterFixedDelay() {
            return Retry.withThrowable(throwableFlux -> throwableFlux
                    .ofType(RateLimitException.class)
                    .flatMap(rateLimitException -> Mono.delay(ofSeconds(rateLimitException.getRetryAfterSeconds())))
                    .switchIfEmpty(Mono.delay(ofSeconds(5))));
        }