Align HttpClient defaults with Spring Framework and provide an opt-out

Update default for `ReactorClientHttpRequestFactoryBuilder` and
`ReactorClientHttpConnectorBuilder` to align with Spring Framework.

Also provide method of opting out in case proxyWithSystemProperties is
not wanted.

Closes gh-49950
This commit is contained in:
Phillip Webb
2026-04-21 22:25:14 -07:00
parent d5aa685ed0
commit f651030f9b
5 changed files with 133 additions and 12 deletions
@@ -96,6 +96,29 @@ public final class ReactorClientHttpRequestFactoryBuilder
this.httpClientBuilder.withHttpClientFactory(factory));
}
/**
* Return a new {@link ReactorClientHttpRequestFactoryBuilder} that does not apply any
* defaults when first creating the {@link HttpClient}.
* @return a new {@link ReactorClientHttpRequestFactoryBuilder} instance
* @since 4.1.0
*/
public ReactorClientHttpRequestFactoryBuilder withoutHttpClientDefaults() {
return withHttpClientDefaults(null);
}
/**
* Return a new {@link ReactorClientHttpRequestFactoryBuilder} that applies the given
* factory defaults when first creating the {@link HttpClient}.
* @param factoryDefaults the factory to use
* @return a new {@link ReactorClientHttpRequestFactoryBuilder} instance
* @since 4.1.0
*/
public ReactorClientHttpRequestFactoryBuilder withHttpClientDefaults(
@Nullable UnaryOperator<HttpClient> factoryDefaults) {
return new ReactorClientHttpRequestFactoryBuilder(getCustomizers(),
this.httpClientBuilder.withHttpClientDefaults(factoryDefaults));
}
/**
* Return a new {@link ReactorClientHttpRequestFactoryBuilder} that applies additional
* customization to the underlying {@link HttpClient}.
@@ -50,17 +50,21 @@ public final class ReactorHttpClientBuilder {
private final Supplier<HttpClient> factory;
private final UnaryOperator<HttpClient> factoryDefaults;
private final UnaryOperator<HttpClient> customizer;
private final @Nullable ResolvedAddressSelector<? super HttpClientConfig> resolvedAddressSelector;
public ReactorHttpClientBuilder() {
this(HttpClient::create, UnaryOperator.identity(), null);
this(HttpClient::create, ReactorHttpClientBuilder::applySpringDefaults, UnaryOperator.identity(), null);
}
private ReactorHttpClientBuilder(Supplier<HttpClient> httpClientFactory, UnaryOperator<HttpClient> customizer,
private ReactorHttpClientBuilder(Supplier<HttpClient> factory, UnaryOperator<HttpClient> factoryDefaults,
UnaryOperator<HttpClient> customizer,
@Nullable ResolvedAddressSelector<? super HttpClientConfig> resolvedAddressSelector) {
this.factory = httpClientFactory;
this.factory = factory;
this.factoryDefaults = factoryDefaults;
this.customizer = customizer;
this.resolvedAddressSelector = resolvedAddressSelector;
}
@@ -74,6 +78,7 @@ public final class ReactorHttpClientBuilder {
public ReactorHttpClientBuilder withReactorResourceFactory(ReactorResourceFactory reactorResourceFactory) {
Assert.notNull(reactorResourceFactory, "'reactorResourceFactory' must not be null");
return new ReactorHttpClientBuilder(() -> HttpClient.create(reactorResourceFactory.getConnectionProvider()),
this.factoryDefaults,
(httpClient) -> this.customizer.apply(httpClient).runOn(reactorResourceFactory.getLoopResources()),
this.resolvedAddressSelector);
}
@@ -86,7 +91,31 @@ public final class ReactorHttpClientBuilder {
*/
public ReactorHttpClientBuilder withHttpClientFactory(Supplier<HttpClient> factory) {
Assert.notNull(factory, "'factory' must not be null");
return new ReactorHttpClientBuilder(factory, this.customizer, this.resolvedAddressSelector);
return new ReactorHttpClientBuilder(factory, this.factoryDefaults, this.customizer,
this.resolvedAddressSelector);
}
/**
* Return a new {@link ReactorHttpClientBuilder} that does not apply any defaults when
* first creating the {@link HttpClient}.
* @return a new {@link ReactorHttpClientBuilder} instance
* @since 4.1.0
*/
public ReactorHttpClientBuilder withoutHttpClientDefaults() {
return withHttpClientDefaults(null);
}
/**
* Return a new {@link ReactorHttpClientBuilder} that applies the given factory
* defaults when first creating the {@link HttpClient}.
* @param factoryDefaults the factory to use
* @return a new {@link ReactorHttpClientBuilder} instance
* @since 4.1.0
*/
public ReactorHttpClientBuilder withHttpClientDefaults(@Nullable UnaryOperator<HttpClient> factoryDefaults) {
return new ReactorHttpClientBuilder(this.factory,
(factoryDefaults != null) ? factoryDefaults : UnaryOperator.identity(), this.customizer,
this.resolvedAddressSelector);
}
/**
@@ -97,7 +126,7 @@ public final class ReactorHttpClientBuilder {
*/
public ReactorHttpClientBuilder withHttpClientCustomizer(UnaryOperator<HttpClient> customizer) {
Assert.notNull(customizer, "'customizer' must not be null");
return new ReactorHttpClientBuilder(this.factory,
return new ReactorHttpClientBuilder(this.factory, this.factoryDefaults,
(httpClient) -> customizer.apply(this.customizer.apply(httpClient)), this.resolvedAddressSelector);
}
@@ -112,7 +141,8 @@ public final class ReactorHttpClientBuilder {
public ReactorHttpClientBuilder withResolvedAddressSelector(
ResolvedAddressSelector<? super HttpClientConfig> resolvedAddressSelector) {
Assert.notNull(resolvedAddressSelector, "'resolvedAddressSelector' must not be null");
return new ReactorHttpClientBuilder(this.factory, this.customizer, resolvedAddressSelector);
return new ReactorHttpClientBuilder(this.factory, this.factoryDefaults, this.customizer,
resolvedAddressSelector);
}
/**
@@ -122,7 +152,7 @@ public final class ReactorHttpClientBuilder {
*/
public HttpClient build(@Nullable HttpClientSettings settings) {
settings = (settings != null) ? settings : HttpClientSettings.defaults();
HttpClient httpClient = applyDefaults(this.factory.get());
HttpClient httpClient = this.factoryDefaults.apply(this.factory.get());
PropertyMapper map = PropertyMapper.get();
httpClient = map.from(settings::connectTimeout).to(httpClient, this::setConnectTimeout);
httpClient = map.from(settings::readTimeout).to(httpClient, HttpClient::responseTimeout);
@@ -146,11 +176,6 @@ public final class ReactorHttpClientBuilder {
: this.resolvedAddressSelector;
}
HttpClient applyDefaults(HttpClient httpClient) {
// Aligns with Spring Framework defaults
return httpClient.compress(true);
}
private HttpClient setConnectTimeout(HttpClient httpClient, Duration timeout) {
return httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) timeout.toMillis());
}
@@ -177,4 +202,9 @@ public final class ReactorHttpClientBuilder {
spec.sslContext(builder.build());
}
static HttpClient applySpringDefaults(HttpClient httpClient) {
// Aligns with Spring Framework defaults in ReactorClientHttpRequestFactory
return httpClient.compress(true).proxyWithSystemProperties();
}
}
@@ -90,6 +90,29 @@ public final class ReactorClientHttpConnectorBuilder
this.httpClientBuilder.withHttpClientFactory(factory));
}
/**
* Return a new {@link ReactorClientHttpConnectorBuilder} that does not apply any
* defaults when first creating the {@link HttpClient}.
* @return a new {@link ReactorClientHttpConnectorBuilder} instance
* @since 4.1.0
*/
public ReactorClientHttpConnectorBuilder withoutHttpClientDefaults() {
return withHttpClientDefaults(null);
}
/**
* Return a new {@link ReactorClientHttpConnectorBuilder} that applies the given
* factory defaults when first creating the {@link HttpClient}.
* @param factoryDefaults the factory to use
* @return a new {@link ReactorClientHttpConnectorBuilder} instance
* @since 4.1.0
*/
public ReactorClientHttpConnectorBuilder withHttpClientDefaults(
@Nullable UnaryOperator<HttpClient> factoryDefaults) {
return new ReactorClientHttpConnectorBuilder(getCustomizers(),
this.httpClientBuilder.withHttpClientDefaults(factoryDefaults));
}
/**
* Return a new {@link ReactorClientHttpConnectorBuilder} that applies additional
* customization to the underlying {@link HttpClient}.
@@ -28,6 +28,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ReactorClientHttpRequestFactory;
import org.springframework.http.client.ReactorResourceFactory;
import org.springframework.test.util.ReflectionTestUtils;
@@ -63,6 +64,29 @@ class ReactorClientHttpRequestFactoryBuilderTests
assertThat(called).containsExactly(true);
}
@Test
void springHttpClientDefaults() {
ClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.reactor().build();
assertThat(factory).extracting("httpClient.config.acceptGzip").isEqualTo(true);
}
@Test
void withoutHttpClientDefaults() {
ClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.reactor()
.withoutHttpClientDefaults()
.build();
assertThat(factory).extracting("httpClient.config.acceptGzip").isEqualTo(false);
}
@Test
void withHttpClientDefaults() {
ClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.reactor()
.withHttpClientDefaults((httpClient) -> httpClient.baseUrl("test"))
.build();
assertThat(factory).extracting("httpClient.config.acceptGzip").isEqualTo(false);
assertThat(factory).extracting("httpClient.config.baseUrl").isEqualTo("test");
}
@Test
void withReactorResourceFactory() {
ReactorResourceFactory resourceFactory = spy(new ReactorResourceFactory());
@@ -64,6 +64,27 @@ class ReactorClientHttpConnectorBuilderTests
assertThat(called).containsExactly(true);
}
@Test
void springHttpClientDefaults() {
ReactorClientHttpConnector connector = ClientHttpConnectorBuilder.reactor().build();
assertThat(connector).extracting("httpClient.config.acceptGzip").isEqualTo(true);
}
@Test
void withoutHttpClientDefaults() {
ReactorClientHttpConnector connector = ClientHttpConnectorBuilder.reactor().withoutHttpClientDefaults().build();
assertThat(connector).extracting("httpClient.config.acceptGzip").isEqualTo(false);
}
@Test
void withHttpClientDefaults() {
ReactorClientHttpConnector connector = ClientHttpConnectorBuilder.reactor()
.withHttpClientDefaults((httpClient) -> httpClient.baseUrl("test"))
.build();
assertThat(connector).extracting("httpClient.config.acceptGzip").isEqualTo(false);
assertThat(connector).extracting("httpClient.config.baseUrl").isEqualTo("test");
}
@Test
void withReactorResourceFactory() {
ReactorResourceFactory resourceFactory = spy(new ReactorResourceFactory());