'Spring WebClient aggregates multiple 401 UNAUTHORIZED error to throw Exceptions.CompositeException

When WebClient calls an external api (with different query params) asynchronously with a wrong token, the first few returns with 401 UNAUTHORIZED and the control flow stops with Exceptions.CompositeException. We've a filter like below added to the WebClient instance that checks for 4xx and 5xx response status code to throw a custom exception

private final ExchangeFilterFunction responseFilter() {
    return ExchangeFilterFunction
            .ofResponseProcessor(response -> response.statusCode().isError() ? error(() -> new CustomException("API Error")) : just(response));

private WebClient buildWebClient(WebClient.Builder builder) {
    return builder
            .filter(responseFilter())
            .build();
}

Now the issue is CustomException doesn't get thrown when the first call returns with an error (401) but rather aggregates a bunch of calls and throws Exceptions.CompositeException. In the logs we can see atleast 3 or 4 401 UNAUTHORIZED before Composite failure. It doesn't throw the expected CustomException when the first one fails. This WebClient call happens inside a Flux.zip and I'm not sure whether that has something to do with the issue.



Solution 1:[1]

When using any of the DelayError variant operators, reactor will collect any exceptions (if there this more than 1) into a CompositeException.

Reactor provide a utility method Exceptions.unwrapMultiple to help unpack a composite exception into a list of throwables if you want to only propagate a subset or single error upstream - java docs

Exceptions.unwrapMultiple

Attempt to unwrap a Throwable into a List of Throwables. This is only done on the condition that said Throwable is a composite exception built by multiple(Throwable...), in which case the list contains the exceptions wrapped as suppressed exceptions in the composite. In any other case, the list only contains the input Throwable (or is empty in case of null input).

Example

Below is a small example using unwrapMultiple to propagate only the first Throwable within the CompositeException.

    Flux<Object> range = Flux.range(1, 10)
            .flatMapDelayError(i -> Mono.error(new IllegalArgumentException(String.format("Error: %s", i))), 10, 1)
            .onErrorResume(throwable -> Mono.error(Exceptions.unwrapMultiple(throwable).get(0)));
    StepVerifier.create(range)
            .expectError(IllegalArgumentException.class)
            .verify();

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Michael McFadyen