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();