From fa8ecccc37ed5ac426f2aab95125cbbd1585f49a Mon Sep 17 00:00:00 2001 From: Hyun Lee Date: Sun, 13 Sep 2026 19:34:29 -0500 Subject: [PATCH] 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 --- .../reference/pages/io/rest-client.adoc | 8 ++++++ .../HttpComponentsHttpAsyncClientBuilder.java | 12 +++++++-- .../HttpClientAutoConfigurationTests.java | 8 +++--- ...onentsClientHttpConnectorBuilderTests.java | 26 +++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc index bc21718cfa6..9beb2f332df 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc @@ -364,6 +364,7 @@ You can use properties to configure aspects such as: * Any default headers that should be sent. * API versioning configuration. * Redirect settings. +* Cookie handling settings. * Connection and read timeouts. * SSL bundles to use. @@ -428,6 +429,7 @@ These include: * Connection Timeouts. * Read Timeouts. * How HTTP redirects should be handled. +* How HTTP cookies should be handled. * 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[]. @@ -442,8 +444,14 @@ spring: connect-timeout: 2s read-timeout: 1s 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]] diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpAsyncClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpAsyncClientBuilder.java index 9c509ae34fd..843620e2ef4 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpAsyncClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpAsyncClientBuilder.java @@ -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.config.ConnectionConfig; 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.HttpAsyncClientBuilder; import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; @@ -178,7 +179,7 @@ public final class HttpComponentsHttpAsyncClientBuilder { .useSystemProperties() .setRedirectStrategy(HttpComponentsRedirectStrategy.get(settings.redirects())) .setConnectionManager(createConnectionManager(settings)) - .setDefaultRequestConfig(createDefaultRequestConfig()); + .setDefaultRequestConfig(createDefaultRequestConfig(settings)); this.customizer.accept(builder); return builder.build(); } @@ -211,8 +212,15 @@ public final class HttpComponentsHttpAsyncClientBuilder { return builder.build(); } - private RequestConfig createDefaultRequestConfig() { + private RequestConfig createDefaultRequestConfig(HttpClientSettings settings) { 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); return builder.build(); } diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfigurationTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfigurationTests.java index 9f003368896..5a7b4bace4f 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfigurationTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfigurationTests.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; 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.InetAddressFilter; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -52,9 +53,10 @@ class HttpClientAutoConfigurationTests { void createsHttpClientSettingsFromProperties() { this.contextRunner .withPropertyValues("spring.http.clients.redirects=dont-follow", "spring.http.clients.connect-timeout=1s", - "spring.http.clients.read-timeout=2s") - .run((context) -> assertThat(context.getBean(HttpClientSettings.class)).isEqualTo(new HttpClientSettings( - null, HttpRedirects.DONT_FOLLOW, Duration.ofSeconds(1), Duration.ofSeconds(2), null))); + "spring.http.clients.read-timeout=2s", "spring.http.clients.cookie-handling=disable") + .run((context) -> assertThat(context.getBean(HttpClientSettings.class)) + .isEqualTo(new HttpClientSettings(HttpCookieHandling.DISABLE, HttpRedirects.DONT_FOLLOW, + Duration.ofSeconds(1), Duration.ofSeconds(2), null))); } @Test diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/HttpComponentsClientHttpConnectorBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/HttpComponentsClientHttpConnectorBuilderTests.java index 6797eb8a5df..513cfb3c474 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/HttpComponentsClientHttpConnectorBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/HttpComponentsClientHttpConnectorBuilderTests.java @@ -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.config.ConnectionConfig; 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.nio.PoolingAsyncClientConnectionManagerBuilder; import org.apache.hc.core5.function.Resolver; import org.apache.hc.core5.http.nio.ssl.TlsStrategy; import org.jspecify.annotations.Nullable; 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.HttpComponentsHttpAsyncClientBuilder; +import org.springframework.boot.http.client.HttpCookieHandling; import org.springframework.boot.http.client.InetAddressFilter; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.classpath.resources.WithPackageResources; @@ -129,6 +133,28 @@ class HttpComponentsClientHttpConnectorBuilderTests .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 protected long connectTimeout(HttpComponentsClientHttpConnector connector) { return getConnectorConfig(connector).getConnectTimeout().toMilliseconds();