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!