Hello!

When using ReactorClientHttpConnectorBuilder I'm not able to provide ConnectionProvider to a HttpClient

Providing ConnectionProvider without ReactorClientHttpConnectorBuilder

webClientBuilder
    .clientConnector(
        ReactorClientHttpConnector(
            HttpClient.create(
                ConnectionProvider.create("foo")
            )
        )
    )
    .build()

Providing ConnectionProvider with ReactorClientHttpConnectorBuilder

webClientBuilder
    .clientConnector(
        reactorClientHttpConnectorBuilder
            .withReactorResourceFactory(
                ReactorResourceFactory().apply {
                    connectionProvider = ConnectionProvider.builder("foo").build()
                }
            )
            .build()
    )
    .build() 

But then I'm getting error: Caused by: java.lang.IllegalArgumentException: 'useGlobalResources' is mutually exclusive with explicitly configured resources

I can also set flag isUseGlobalResources to false:

reactorClientHttpConnectorBuilder
    .withReactorResourceFactory(
        ReactorResourceFactory().apply {
            isUseGlobalResources = false 
            connectionProvider = ConnectionProvider.builder("foo").build()
        }
    )

But then I had two issues with it: - I need to manually use reactorResourceFactory.stop() - for every webClient instance, I have dedicated threads

It's ok for me to use isUseGlobalResources = false and provide custom LoopResources But not in this case, when I want to set only ConnectionProvider

Expected behaviour

webClientBuilder
    .clientConnector(
        reactorClientHttpConnectorBuilder
            .withConnectionProvider(
                ConnectionProvider.builder("foo").build()
            )
            .build()
    )
    .build()

Version

Spring Boot: 3.5.4

How to reproduce

package com.nalepa.demo

import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.http.client.reactive.ReactorClientHttpConnectorBuilder
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.client.ReactorResourceFactory
import org.springframework.http.client.reactive.ReactorClientHttpConnector
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono
import reactor.netty.http.client.HttpClient
import reactor.netty.resources.ConnectionProvider

@SpringBootTest
class CustomReactorClientHttpConnectorBuilderTest {

    @Autowired
    lateinit var webClientBuilder: WebClient.Builder

    @Autowired
    lateinit var reactorClientHttpConnectorBuilder: ReactorClientHttpConnectorBuilder

    @Test
    fun `OK-SCENARIO-noReactorBuilderUsed`() {
        createWebClientWithoutReactorClientHttpConnectorBuilderAndSentRequests("first")
        val currentThreadsAfterFirstClient = Thread.getAllStackTraces().keys.size

        createWebClientWithoutReactorClientHttpConnectorBuilderAndSentRequests("second")
        val currentThreadsAfterSecondClient = Thread.getAllStackTraces().keys.size

        assert(currentThreadsAfterFirstClient == currentThreadsAfterSecondClient)
    }

    @Test
    fun `FAILING-SCENARIO-reactorBuilderOnlyWithConnectionProviderUsed`() {
        lateinit var throwable: Throwable
        try {
            webClientBuilder
                .clientConnector(
                    reactorClientHttpConnectorBuilder
                        .withReactorResourceFactory(
                            ReactorResourceFactory().apply {
                                connectionProvider = ConnectionProvider.builder("foo").build()
                            }
                        )
                        .build()
                )
                .build()
        } catch (t: Throwable) {
            throwable = t
        }
        // sad :(
        assert(throwable.message == "'useGlobalResources' is mutually exclusive with explicitly configured resources")
    }

    @Test
    fun `FAILING-SCENARIO-reactorBuilderWithConnectionProviderAndIsUseGlobalResourcesTrue`() {
        lateinit var throwable: Throwable
        try {
            webClientBuilder
                .clientConnector(
                    reactorClientHttpConnectorBuilder
                        .withReactorResourceFactory(
                            ReactorResourceFactory().apply {
                                isUseGlobalResources = true
                                connectionProvider = ConnectionProvider.builder("foo").build()
                            }
                        )
                        .build()
                )
                .build()
        } catch (t: Throwable) {
            throwable = t
        }
        // sad :(
        assert(throwable.message == "'useGlobalResources' is mutually exclusive with explicitly configured resources")
    }

    @Test
    fun `NOT-OK-SCENARIO-reactorBuilderWithConnectionProviderAndIsUseGlobalResourcesFalse`() {
        createWebClientWithGlobalResourceSetToFalseAndSentRequests("first")
        val currentThreadsAfterFirstClient = Thread.getAllStackTraces().keys.size

        createWebClientWithGlobalResourceSetToFalseAndSentRequests("second")
        val currentThreadsAfterSecondClient = Thread.getAllStackTraces().keys.size

        assert(
            currentThreadsAfterSecondClient == currentThreadsAfterFirstClient + Runtime.getRuntime()
                .availableProcessors()
        )
        // I want: currentThreadsAfterSecondClient == currentThreadsAfterSecondClient
    }

    private fun createWebClientWithoutReactorClientHttpConnectorBuilderAndSentRequests(connectionName: String) {
        webClientBuilder
            .clientConnector(
                ReactorClientHttpConnector(
                    HttpClient.create(
                        ConnectionProvider.create(connectionName)
                    )
                )
            )
            .build()
            .get().uri("http://localhost:8080/whatever-it-will-fail").retrieve().bodyToMono(String::class.java)
            .onErrorResume { Mono.just("foo") }
            .block()
    }

    private fun createWebClientWithGlobalResourceSetToFalseAndSentRequests(connectionName: String) {
        webClientBuilder
            .clientConnector(
                reactorClientHttpConnectorBuilder
                    .withReactorResourceFactory(
                        ReactorResourceFactory().apply {
                            isUseGlobalResources = false
                            connectionProvider = ConnectionProvider.builder(connectionName).build()
                        }
                    )
                    .build()
            )
            .build()
            .get().uri("http://localhost:8080/whatever-it-will-fail").retrieve().bodyToMono(String::class.java)
            .onErrorResume { Mono.just("foo") }
            .block()
    }

}

If there is something missing, please let me know, I'm open to your feedback!