Apply cookie handling to reactive HttpComponents connector

The spring.http.clients.cookie-handling property and
HttpClientSettings.cookieHandling() were honored by every imperative
ClientHttpRequestFactoryBuilder and by the Jetty, JDK and Reactor
ClientHttpConnectorBuilders, but HttpComponentsHttpAsyncClientBuilder
ignored the setting. As a result, a WebClient backed by Apache
HttpComponents kept storing cookies even when cookie handling was
disabled.

Map the setting to the default request config's cookie spec, as
HttpComponentsHttpClientBuilder already does, and document the property
alongside the other global HTTP client settings.

See gh-51724

Signed-off-by: Hyun Lee <dlwhdugs4147@gmail.com>
This commit is contained in:
Hyun Lee
2026-09-15 13:22:25 +01:00
committed by Andy Wilkinson
parent a8f1a99f7d
commit fa8ecccc37
4 changed files with 49 additions and 5 deletions
@@ -364,6 +364,7 @@ You can use properties to configure aspects such as:
* Any default headers that should be sent. * Any default headers that should be sent.
* API versioning configuration. * API versioning configuration.
* Redirect settings. * Redirect settings.
* Cookie handling settings.
* Connection and read timeouts. * Connection and read timeouts.
* SSL bundles to use. * SSL bundles to use.
@@ -428,6 +429,7 @@ These include:
* Connection Timeouts. * Connection Timeouts.
* Read Timeouts. * Read Timeouts.
* How HTTP redirects should be handled. * How HTTP redirects should be handled.
* How HTTP cookies should be handled.
* Which SSL bundle should be used when connecting. * Which SSL bundle should be used when connecting.
These common settings are represented by the javadoc:org.springframework.boot.http.client.HttpClientSettings[] class which can be passed into the `build(...)` methods of javadoc:org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder[] and javadoc:org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder[]. These common settings are represented by the javadoc:org.springframework.boot.http.client.HttpClientSettings[] class which can be passed into the `build(...)` methods of javadoc:org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder[] and javadoc:org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder[].
@@ -442,8 +444,14 @@ spring:
connect-timeout: 2s connect-timeout: 2s
read-timeout: 1s read-timeout: 1s
redirects: dont-follow redirects: dont-follow
cookie-handling: disable
---- ----
When `spring.http.clients.cookie-handling` is not set, the default cookie handling of the underlying HTTP client library is used.
These defaults vary: for example, the Apache HttpComponents and Jetty clients store cookies and send them with subsequent requests whereas the JDK client ignores them.
When cookie handling is enabled, each underlying HTTP client instance uses its own cookie store.
Reactor Netty and the `simple` request factory do not support cookie handling so `enable` will fail with those clients and `enable-when-possible` will leave them unchanged.
[[io.rest-client.global-configuration.inetaddress-filtering]] [[io.rest-client.global-configuration.inetaddress-filtering]]
@@ -26,6 +26,7 @@ import org.apache.hc.client5.http.SystemDefaultDnsResolver;
import org.apache.hc.client5.http.async.HttpAsyncClient; import org.apache.hc.client5.http.async.HttpAsyncClient;
import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.cookie.StandardCookieSpec;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
@@ -178,7 +179,7 @@ public final class HttpComponentsHttpAsyncClientBuilder {
.useSystemProperties() .useSystemProperties()
.setRedirectStrategy(HttpComponentsRedirectStrategy.get(settings.redirects())) .setRedirectStrategy(HttpComponentsRedirectStrategy.get(settings.redirects()))
.setConnectionManager(createConnectionManager(settings)) .setConnectionManager(createConnectionManager(settings))
.setDefaultRequestConfig(createDefaultRequestConfig()); .setDefaultRequestConfig(createDefaultRequestConfig(settings));
this.customizer.accept(builder); this.customizer.accept(builder);
return builder.build(); return builder.build();
} }
@@ -211,8 +212,15 @@ public final class HttpComponentsHttpAsyncClientBuilder {
return builder.build(); return builder.build();
} }
private RequestConfig createDefaultRequestConfig() { private RequestConfig createDefaultRequestConfig(HttpClientSettings settings) {
RequestConfig.Builder builder = RequestConfig.custom(); RequestConfig.Builder builder = RequestConfig.custom();
if (settings.cookieHandling() != null) {
String cookieSpec = switch (settings.cookieHandling()) {
case ENABLE_WHEN_POSSIBLE, ENABLE -> StandardCookieSpec.STRICT;
case DISABLE -> StandardCookieSpec.IGNORE;
};
builder.setCookieSpec(cookieSpec);
}
this.defaultRequestConfigCustomizer.accept(builder); this.defaultRequestConfigCustomizer.accept(builder);
return builder.build(); return builder.build();
} }
@@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpClientSettings;
import org.springframework.boot.http.client.HttpCookieHandling;
import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.http.client.HttpRedirects;
import org.springframework.boot.http.client.InetAddressFilter; import org.springframework.boot.http.client.InetAddressFilter;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -52,9 +53,10 @@ class HttpClientAutoConfigurationTests {
void createsHttpClientSettingsFromProperties() { void createsHttpClientSettingsFromProperties() {
this.contextRunner this.contextRunner
.withPropertyValues("spring.http.clients.redirects=dont-follow", "spring.http.clients.connect-timeout=1s", .withPropertyValues("spring.http.clients.redirects=dont-follow", "spring.http.clients.connect-timeout=1s",
"spring.http.clients.read-timeout=2s") "spring.http.clients.read-timeout=2s", "spring.http.clients.cookie-handling=disable")
.run((context) -> assertThat(context.getBean(HttpClientSettings.class)).isEqualTo(new HttpClientSettings( .run((context) -> assertThat(context.getBean(HttpClientSettings.class))
null, HttpRedirects.DONT_FOLLOW, Duration.ofSeconds(1), Duration.ofSeconds(2), null))); .isEqualTo(new HttpClientSettings(HttpCookieHandling.DISABLE, HttpRedirects.DONT_FOLLOW,
Duration.ofSeconds(1), Duration.ofSeconds(2), null)));
} }
@Test @Test
@@ -25,15 +25,19 @@ import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.async.HttpAsyncClient; import org.apache.hc.client5.http.async.HttpAsyncClient;
import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.cookie.StandardCookieSpec;
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
import org.apache.hc.core5.function.Resolver; import org.apache.hc.core5.function.Resolver;
import org.apache.hc.core5.http.nio.ssl.TlsStrategy; import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpClientSettings;
import org.springframework.boot.http.client.HttpComponentsHttpAsyncClientBuilder; import org.springframework.boot.http.client.HttpComponentsHttpAsyncClientBuilder;
import org.springframework.boot.http.client.HttpCookieHandling;
import org.springframework.boot.http.client.InetAddressFilter; import org.springframework.boot.http.client.InetAddressFilter;
import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources; import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
@@ -129,6 +133,28 @@ class HttpComponentsClientHttpConnectorBuilderTests
.isSameAs(dnsResolver); .isSameAs(dnsResolver);
} }
@Test
void defaultCookieHandling() {
HttpComponentsClientHttpConnector connector = ClientHttpConnectorBuilder.httpComponents()
.build(HttpClientSettings.defaults());
assertThat(connector).extracting("client.defaultConfig.cookieSpec").isNull();
}
@Test
void cookieHandlingDisabled() {
HttpComponentsClientHttpConnector connector = ClientHttpConnectorBuilder.httpComponents()
.build(HttpClientSettings.defaults().withCookieHandling(HttpCookieHandling.DISABLE));
assertThat(connector).extracting("client.defaultConfig.cookieSpec").isEqualTo(StandardCookieSpec.IGNORE);
}
@ParameterizedTest
@EnumSource(names = { "ENABLE", "ENABLE_WHEN_POSSIBLE" })
void cookieHandlingEnabled(HttpCookieHandling cookieHandling) {
HttpComponentsClientHttpConnector connector = ClientHttpConnectorBuilder.httpComponents()
.build(HttpClientSettings.defaults().withCookieHandling(cookieHandling));
assertThat(connector).extracting("client.defaultConfig.cookieSpec").isEqualTo(StandardCookieSpec.STRICT);
}
@Override @Override
protected long connectTimeout(HttpComponentsClientHttpConnector connector) { protected long connectTimeout(HttpComponentsClientHttpConnector connector) {
return getConnectorConfig(connector).getConnectTimeout().toMilliseconds(); return getConnectorConfig(connector).getConnectTimeout().toMilliseconds();