From f651030f9bdf2dd8e97f5511e23bcc43e58faaeb Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 21 Apr 2026 22:25:14 -0700 Subject: [PATCH] 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 --- ...eactorClientHttpRequestFactoryBuilder.java | 23 ++++++++ .../http/client/ReactorHttpClientBuilder.java | 54 ++++++++++++++----- .../ReactorClientHttpConnectorBuilder.java | 23 ++++++++ ...rClientHttpRequestFactoryBuilderTests.java | 24 +++++++++ ...eactorClientHttpConnectorBuilderTests.java | 21 ++++++++ 5 files changed, 133 insertions(+), 12 deletions(-) diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java index 35d3d61304e..cc2baedde8a 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java @@ -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 factoryDefaults) { + return new ReactorClientHttpRequestFactoryBuilder(getCustomizers(), + this.httpClientBuilder.withHttpClientDefaults(factoryDefaults)); + } + /** * Return a new {@link ReactorClientHttpRequestFactoryBuilder} that applies additional * customization to the underlying {@link HttpClient}. diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java index c5dc7329c03..4dabc567087 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java @@ -50,17 +50,21 @@ public final class ReactorHttpClientBuilder { private final Supplier factory; + private final UnaryOperator factoryDefaults; + private final UnaryOperator customizer; private final @Nullable ResolvedAddressSelector resolvedAddressSelector; public ReactorHttpClientBuilder() { - this(HttpClient::create, UnaryOperator.identity(), null); + this(HttpClient::create, ReactorHttpClientBuilder::applySpringDefaults, UnaryOperator.identity(), null); } - private ReactorHttpClientBuilder(Supplier httpClientFactory, UnaryOperator customizer, + private ReactorHttpClientBuilder(Supplier factory, UnaryOperator factoryDefaults, + UnaryOperator customizer, @Nullable ResolvedAddressSelector 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 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 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 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 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(); + } + } diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilder.java index e8ab415456e..4105ee1c268 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilder.java @@ -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 factoryDefaults) { + return new ReactorClientHttpConnectorBuilder(getCustomizers(), + this.httpClientBuilder.withHttpClientDefaults(factoryDefaults)); + } + /** * Return a new {@link ReactorClientHttpConnectorBuilder} that applies additional * customization to the underlying {@link HttpClient}. diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilderTests.java index 2a60d2cd6b8..7bb64d95c3d 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilderTests.java @@ -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()); diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilderTests.java index e48bb4c3ecb..eb44ca46e96 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilderTests.java @@ -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());