Describe the bug

In some configuration setups, adding a ServletOAuth2AuthorizedClientExchangeFilterFunction to a WebClient that can retry causes an IllegalArgumentException when trying to remove the client in the case where the WebClient receives a response actionable by the authorization failure forwarder. The ClientRequest is not always populated with a HttpServletRequest attribute when the filter is added via the WebClientBuilder.filter() method instead of using the the filter's filter.oauth2Configuration() method.

The filter.filter() method does merge the required objects into the ClientRequest when making an authorized client, but then does not use that merged request when executing the handler within the filter. Without the merged attributes, its impossible for the AuthorizationFailureForwarder to clean up the client in case there is a 401/403 response from the original ClientRequest.

To Reproduce * Create a blank MVC-based project * Create a WebClient * Attach ServletOAuth2AuthorizedClientExchangeFilterFunction to it via builder.filter() * Make sure the WebClient is set to retry on WebClientResponseExceptions for 401 or 403 responses. * Make sure the ServletOAuth2AuthorizedClientExchangeFilterFunction is able to retrieve a token successfully * Make sure the WebClient is able to call an endpoint that will fail with 401 or 403 even if the token is valid to simulate the situation. * WebClient should emit an IllegalArgumentException with "request must not be null" as a message.

Expected behavior

It is expected that the authorizedClient is removed via the AuthorizationFailureForwarder in the case a webclient with a ServletOAuth2AuthorizedClientExchangeFilterFunction.

Sample

No sample available yet. Working to extract a minimal setup from my own programs.

Comment From: jjstreet

@Override
    public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
        // @formatter:off
        return mergeRequestAttributesIfNecessary(request)
                .filter((req) -> req.attribute(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME).isPresent())
                .flatMap((req) -> reauthorizeClient(getOAuth2AuthorizedClient(req.attributes()), req))
                .switchIfEmpty(
                        Mono.defer(() ->
                            mergeRequestAttributesIfNecessary(request)
                                .filter((req) -> resolveClientRegistrationId(req) != null)
                                .flatMap((req) -> authorizeClient(resolveClientRegistrationId(req), req))
                        )
                )
                .map((authorizedClient) -> bearer(request, authorizedClient))
                .flatMap((requestWithBearer) -> exchangeAndHandleResponse(requestWithBearer, next))
                .switchIfEmpty(Mono.defer(() -> exchangeAndHandleResponse(request, next)));
        // @formatter:on
    }

The problem is the ClientRequest that is created by mergeRequestAttributesIfNecessary() is not passed to exchangeAndHandleResponse and thus, situations where the client must be cleaned up fail with an IllegalArgumentException

Comment From: jjstreet

By adjusting the filter methods as shown, the merged request is used by the exchangeAndHandleResponse methods. Then the client can be cleaned up properly.

@Override
    public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
        return mergeRequestAttributesIfNecessary(request)
                .flatMap(merged -> Mono.just(merged)
                        .filter((req) -> req.attribute(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME).isPresent())
                        .flatMap((req) -> reauthorizeClient(getOAuth2AuthorizedClient(req.attributes()), req))
                        .switchIfEmpty(
                                Mono.just(merged)
                                        .filter((req) -> resolveClientRegistrationId(req) != null)
                                        .flatMap((req) -> authorizeClient(resolveClientRegistrationId(req), req)))
                        .map((authorizedClient) -> bearer(merged, authorizedClient))
                        .flatMap((requestWithBearer) -> exchangeAndHandleResponse(requestWithBearer, next))
                        .switchIfEmpty(exchangeAndHandleResponse(merged, next)));
    }

As you can see, I flatMap on the merging so that I can use it in rest of the pipeline, which includes the the points where the exchange is handled.

Comment From: jjstreet

The mergeRequestAttributesIfNecessary() is what is confusing here, because if we use the convenience method to initially set up the filter on the webClient, it adds the the request/response objects to the client request attributes. There is no reason to do so again?

With the proposed fix, it might not be necessary to have the convenience method filter.oauth2Configuration(). It seems to be the only oauth2 filter function that has it, as the reactive variants do not. But I also do not know if there is any problem with the reactive variants either (though i doubt it, given there are no servlet requests objects to attach).