spring-bootspring-webfluxspring-webclient

Extracting response body from error response when using Webclient synchronously with retries


What am I trying to do:

Replace an old RestTemplate call with WebClient synchronous call , as per Spring recommendation to avoid using RestTemplate and as part of SpringBoot upgrade.


What behavior I want to achieve:

  1. Synchronous call
  2. Retry up to 3 times
  3. Don't retry if the error is 4xx
  4. When retries are exhausted or if decided not to retry due to 4xx - throw a specific type of custom exception. That exception should contain status code and response body (since the service to which the request is sent returns a meaningful error message in the response body).

What I coded:

  1. Using onStatus to translate 4xx to ApiInvokerClientErrorException and to translate other non 2xx status codes to ApiInvokerErrorException, while passing the status code and response body as parameters to those exceptions.
  2. Using filter and ApiInvokerClientErrorException class to avoid retry in case of 4xx error
  3. Using onRetryExhaustedThrow to extract status code and response body from ApiInvokerErrorException/ApiInvokerClientErrorException and build the required dedicated exception that is thrown.

The code:

String responseJson = WebClient.create()
        .method(httpMethod)
        .uri(url, uriBuilder -> uriBuilder.queryParams(queryParams).build())
        .bodyValue(body)
        .retrieve()
        .onStatus(status -> status.is4xxClientError(),
                response -> Mono.error(new ApiInvokerClientErrorException(response.bodyToMono(String.class).block(Duration.ZERO), response.statusCode())))
        .onStatus(status -> (!status.is2xxSuccessful() && !status.is4xxClientError()),
                response -> Mono.error(new ApiInvokerException(response.bodyToMono(String.class).block(Duration.ZERO), response.statusCode())))
        .bodyToMono(String.class)
        .retryWhen(Retry.fixedDelay(3, Duration.ofMillis(delayBetweenRetriesInMillis))
                .filter(throwable -> !(throwable instanceof ApiInvokerClientErrorException))
                .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
                    if (retrySignal.failure() instanceof ApiInvokerException e) {
                        throw new UniformExceptionThatNeedToBeThrownOnError(e.getResponseBody(), e.getHttpStatus());
                    }
                    else {
                        throw new RuntimeException("The following call failed after 3 attempts: " + httpMethod.name() + url, retrySignal.failure());
                    }
                }))
        .block();   

The exception classes are implemented as following:

public class ApiInvokerException extends Exception {
        private String responseBody;
        private HttpStatusCode httpStatusCode;
    
        public ApiInvokerException(String responseBody, HttpStatusCode statusCode) {
            super("Some message);
            this.responseBody = responseBody;
            this.httpStatusCode = statusCode.value();
        }
}
    
public class ApiInvokerClientErrorException extends ApiInvokerException {
    
        public ApiInvokerClientErrorException(String responseBody, HttpStatusCode statusCode) {
           super("Some message", responseBody, statusCode);
        }
}

Problem: It is not a working solution since I can't block in onStatus (getting java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-epoll-2)

Question: How can I extract response body from error so I can add it to thrown exception when number of retries is exhausted (either by reaching 3 or by reaching not retrying due to 4xx code)


Solution

  • Consider using WebClientResponseException, example code:

    Mono<String> responseJson = WebClient.create()
            .method(HttpMethod.GET)
            .uri(u -> u.scheme("http").host("localhost").port(8080).path("test").build())
            .retrieve()
            .bodyToMono(String.class)
            .retryWhen(
                    Retry.fixedDelay(3, Duration.ofMillis(1000))
                            .filter(throwable -> !(throwable instanceof WebClientResponseException ex
                                    && ex.getStatusCode().is4xxClientError())
                            )
                            .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> retrySignal.failure())
            );
    StepVerifier.create(responseJson)
            .consumeErrorWith(ex -> {
                WebClientResponseException webClientResponseException = (WebClientResponseException) ex;
                System.out.println(ex);
                System.out.println(webClientResponseException.getResponseBodyAsString());
            })
            .verify();