Merge pull request #51724 from dlwhdgus0810

Closes gh-51724

* gh-51724:
  Apply cookie handling to reactive HttpComponents connector
This commit is contained in:
Andy Wilkinson
2026-09-15 13:30:44 +01:00
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.
* 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]]
@@ -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();
}
@@ -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
@@ -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();