spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
56.45k stars 38.09k forks source link

Low level exception handling in HTTP Interface Clients #33353

Closed ZIRAKrezovic closed 1 month ago

ZIRAKrezovic commented 2 months ago

Affects: 6.1.11

I am following https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#rest-http-interface-exceptions and I'm trying to map exceptions from all 3 supported adapter clients to ones that do not have a dependency on concrete web stack dependency.

For handling response from WebFlux WebClient, the following is recommended in the linked guide

By default, WebClient raises WebClientResponseException for 4xx and 5xx HTTP status codes. To customize this, register a response status handler that applies to all responses performed through the client:

WebClient webClient = WebClient.builder()
        .defaultStatusHandler(HttpStatusCode::isError, resp -> ...)
        .build();

However, this does not cover the low level exception , org.springframework.web.reactive.function.client.WebClientRequestException. Simplest way to trigger it is to give non-existing hostname as a URL.

Is there a possibility to somehow plug in into the exchange function and re-map the org.springframework.web.reactive.function.client.WebClientRequestException to something else? I am not able to find a way with Spring Framework 6.1.11

If I understand correctly, this is the equivalent of ResourceAccessException usually thrown in non-reactive RestClient(RestTemplate).

I have tried

var client = WebClient.builder()
                .filter(ExchangeFilterFunction.ofRequestProcessor(request -> Mono.just(request)))
                .filter(ExchangeFilterFunction.ofResponseProcessor(response -> Mono.just(response)))
                .defaultStatusHandler(HttpStatusCode::isError, SpringWebCompatibleStatusHandler::handle)
                .build();

var adapter =  WebClientAdapter.create(client);

Where SpringWebCompatibleStatusHandler is as follows

public final class SpringWebCompatibleStatusHandler {
    private SpringWebCompatibleStatusHandler() {}

    public static Mono<Throwable> handle(@NonNull ClientResponse response) {
        return response.createException().map(SpringWebCompatibleStatusHandler::map);
    }

    private static Throwable map(WebClientResponseException ex) {
        var statusCode = ex.getStatusCode();
        var statusText = ex.getStatusText();
        var headers = ex.getHeaders();
        var message = ex.getMessage();
        var body = ex.getResponseBodyAsByteArray();

        var charset =
                Optional.ofNullable(headers.getContentType())
                        .map(MediaType::getCharset)
                        .orElse(null);

        RestClientResponseException throwable;

        if (statusCode.is4xxClientError()) {
            throwable =
                    HttpClientErrorException.create(
                            message, statusCode, statusText, headers, body, charset);
        } else if (statusCode.is5xxServerError()) {
            throwable =
                    HttpServerErrorException.create(
                            message, statusCode, statusText, headers, body, charset);
        } else {
            throwable =
                    new UnknownHttpStatusCodeException(
                            message, statusCode.value(), statusText, headers, body, charset);
        }

        return throwable;
    }
}

None of the functions is called before or after I get WebClientRequestException

Caused by: org.springframework.web.reactive.function.client.WebClientRequestException: Failed to resolve 'ua-integration' [A(1)]
    at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.lambda$wrapException$9(ExchangeFunctions.java:136)
    Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below: 
Assembly trace from producer [reactor.core.publisher.MonoErrorSupplied] :
    reactor.core.publisher.Mono.error(Mono.java:315)
    org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.wrapException(ExchangeFunctions.java:136)
rstoyanchev commented 1 month ago

A filter provides full control, including handling of early exception like WebClientRequestException. However, ExchangeFilterFunction#ofResponseProcessor isn't the right hook. The flatMap here is called if the exchange produces a response. What you need instead is to handle an error signal.

For example:

ResponseEntity<String> entity = WebClient.builder()
        .filter((request, next) ->
                next.exchange(request)
                        .onErrorResume(WebClientRequestException.class, ex -> {
                            System.out.println("Got: " + ex.getMessage());
                            return Mono.error(ex);
                        }))
        .build()
        .get().uri("http://invalid/path")
        .retrieve()
        .toEntity(String.class)
        .block();

Results in:

19:45:54.246 [Test worker] DEBUG o.s.w.r.f.c.ExchangeFunctions - [350ec690] HTTP GET http://invalid/path
Got: Failed to resolve 'invalid' [A(1)]

The above just intercepts, prints, and propagates the same exception, but you can handle it in any way you want, or user another error operator as needed.