diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java index 5f35ff04e432..03db7cf569c5 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java @@ -25,6 +25,8 @@ /** * Settings that can be applied when creating an imperative or reactive HTTP client. * + * @param cookies the cookie handling strategy to use or null to use the underlying + * library's default * @param redirects the follow redirect strategy to use or null to redirect whenever the * underlying library allows it * @param connectTimeout the connect timeout @@ -33,10 +35,21 @@ * @author Phillip Webb * @since 3.5.0 */ -public record HttpClientSettings(@Nullable HttpRedirects redirects, @Nullable Duration connectTimeout, - @Nullable Duration readTimeout, @Nullable SslBundle sslBundle) { +public record HttpClientSettings(@Nullable HttpCookies cookies, @Nullable HttpRedirects redirects, + @Nullable Duration connectTimeout, @Nullable Duration readTimeout, @Nullable SslBundle sslBundle) { - private static final HttpClientSettings defaults = new HttpClientSettings(null, null, null, null); + private static final HttpClientSettings defaults = new HttpClientSettings(null, null, null, null, null); + + /** + * Return a new {@link HttpClientSettings} instance with an updated cookie handling + * setting. + * @param cookies the new cookie handling setting + * @return a new {@link HttpClientSettings} instance + * @since 4.1.0 + */ + public HttpClientSettings withCookies(@Nullable HttpCookies cookies) { + return new HttpClientSettings(cookies, this.redirects, this.connectTimeout, this.readTimeout, this.sslBundle); + } /** * Return a new {@link HttpClientSettings} instance with an updated connect timeout @@ -46,7 +59,7 @@ public record HttpClientSettings(@Nullable HttpRedirects redirects, @Nullable Du * @since 4.0.0 */ public HttpClientSettings withConnectTimeout(@Nullable Duration connectTimeout) { - return new HttpClientSettings(this.redirects, connectTimeout, this.readTimeout, this.sslBundle); + return new HttpClientSettings(this.cookies, this.redirects, connectTimeout, this.readTimeout, this.sslBundle); } /** @@ -57,7 +70,7 @@ public HttpClientSettings withConnectTimeout(@Nullable Duration connectTimeout) * @since 4.0.0 */ public HttpClientSettings withReadTimeout(@Nullable Duration readTimeout) { - return new HttpClientSettings(this.redirects, this.connectTimeout, readTimeout, this.sslBundle); + return new HttpClientSettings(this.cookies, this.redirects, this.connectTimeout, readTimeout, this.sslBundle); } /** @@ -69,7 +82,7 @@ public HttpClientSettings withReadTimeout(@Nullable Duration readTimeout) { * @since 4.0.0 */ public HttpClientSettings withTimeouts(@Nullable Duration connectTimeout, @Nullable Duration readTimeout) { - return new HttpClientSettings(this.redirects, connectTimeout, readTimeout, this.sslBundle); + return new HttpClientSettings(this.cookies, this.redirects, connectTimeout, readTimeout, this.sslBundle); } /** @@ -80,7 +93,7 @@ public HttpClientSettings withTimeouts(@Nullable Duration connectTimeout, @Nulla * @since 4.0.0 */ public HttpClientSettings withSslBundle(@Nullable SslBundle sslBundle) { - return new HttpClientSettings(this.redirects, this.connectTimeout, this.readTimeout, sslBundle); + return new HttpClientSettings(this.cookies, this.redirects, this.connectTimeout, this.readTimeout, sslBundle); } /** @@ -90,7 +103,7 @@ public HttpClientSettings withSslBundle(@Nullable SslBundle sslBundle) { * @since 4.0.0 */ public HttpClientSettings withRedirects(@Nullable HttpRedirects redirects) { - return new HttpClientSettings(redirects, this.connectTimeout, this.readTimeout, this.sslBundle); + return new HttpClientSettings(this.cookies, redirects, this.connectTimeout, this.readTimeout, this.sslBundle); } /** @@ -104,11 +117,12 @@ public HttpClientSettings orElse(@Nullable HttpClientSettings other) { if (other == null) { return this; } + HttpCookies cookies = (cookies() != null) ? cookies() : other.cookies(); HttpRedirects redirects = (redirects() != null) ? redirects() : other.redirects(); Duration connectTimeout = (connectTimeout() != null) ? connectTimeout() : other.connectTimeout(); Duration readTimeout = (readTimeout() != null) ? readTimeout() : other.readTimeout(); SslBundle sslBundle = (sslBundle() != null) ? sslBundle() : other.sslBundle(); - return new HttpClientSettings(redirects, connectTimeout, readTimeout, sslBundle); + return new HttpClientSettings(cookies, redirects, connectTimeout, readTimeout, sslBundle); } /** diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsCookieSpec.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsCookieSpec.java new file mode 100644 index 000000000000..a597f36c52c4 --- /dev/null +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsCookieSpec.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import org.apache.hc.client5.http.cookie.StandardCookieSpec; +import org.jspecify.annotations.Nullable; + +/** + * Adapts {@link HttpCookies} to an + * Apache HttpComponents + * cookie spec identifier. + * + * @author Apoorv Darshan + */ +final class HttpComponentsCookieSpec { + + private HttpComponentsCookieSpec() { + } + + static @Nullable String get(@Nullable HttpCookies cookies) { + if (cookies == null) { + return null; + } + return switch (cookies) { + case ENABLE_WHEN_POSSIBLE, ENABLE -> StandardCookieSpec.STRICT; + case DISABLE -> StandardCookieSpec.IGNORE; + }; + } + +} diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java index 5e3f8b5c228e..cb1653568cde 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java @@ -180,7 +180,7 @@ public CloseableHttpClient build(@Nullable HttpClientSettings settings) { .useSystemProperties() .setRedirectStrategy(HttpComponentsRedirectStrategy.get(settings.redirects())) .setConnectionManager(createConnectionManager(settings)) - .setDefaultRequestConfig(createDefaultRequestConfig()); + .setDefaultRequestConfig(createDefaultRequestConfig(settings)); this.customizer.accept(builder); return builder.build(); } @@ -218,8 +218,12 @@ private ConnectionConfig createConnectionConfig(HttpClientSettings settings) { return builder.build(); } - private RequestConfig createDefaultRequestConfig() { + private RequestConfig createDefaultRequestConfig(HttpClientSettings settings) { RequestConfig.Builder builder = RequestConfig.custom(); + String cookieSpec = HttpComponentsCookieSpec.get(settings.cookies()); + if (cookieSpec != null) { + builder.setCookieSpec(cookieSpec); + } this.defaultRequestConfigCustomizer.accept(builder); return builder.build(); } diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpCookies.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpCookies.java new file mode 100644 index 000000000000..0f2ba8162dbf --- /dev/null +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpCookies.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +/** + * Cookie handling strategies supported by HTTP clients. + * + * @author Apoorv Darshan + * @since 4.1.0 + */ +public enum HttpCookies { + + /** + * Enable cookies (if the underlying library has support). + */ + ENABLE_WHEN_POSSIBLE, + + /** + * Enable cookies (fail if the underlying library has no support). + */ + ENABLE, + + /** + * Disable cookies (fail if the underlying library has no support). + */ + DISABLE + +} diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpClientSettingsTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpClientSettingsTests.java index d37f796e772d..c7b4d1af1709 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpClientSettingsTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpClientSettingsTests.java @@ -47,7 +47,8 @@ void defaults() { @Test void createWithNulls() { - HttpClientSettings settings = new HttpClientSettings(null, null, null, null); + HttpClientSettings settings = new HttpClientSettings(null, null, null, null, null); + assertThat(settings.cookies()).isNull(); assertThat(settings.redirects()).isNull(); assertThat(settings.connectTimeout()).isNull(); assertThat(settings.readTimeout()).isNull(); @@ -82,6 +83,16 @@ void withSslBundleReturnsInstanceWithUpdatedSslBundle() { assertThat(settings.sslBundle()).isSameAs(sslBundle); } + @Test + void withCookiesReturnsInstanceWithUpdatedCookies() { + HttpClientSettings settings = HttpClientSettings.defaults().withCookies(HttpCookies.DISABLE); + assertThat(settings.cookies()).isEqualTo(HttpCookies.DISABLE); + assertThat(settings.redirects()).isNull(); + assertThat(settings.connectTimeout()).isNull(); + assertThat(settings.readTimeout()).isNull(); + assertThat(settings.sslBundle()).isNull(); + } + @Test void withRedirectsReturnsInstanceWithUpdatedRedirect() { HttpClientSettings settings = HttpClientSettings.defaults().withRedirects(HttpRedirects.DONT_FOLLOW); @@ -94,8 +105,10 @@ void withRedirectsReturnsInstanceWithUpdatedRedirect() { @Test void orElseReturnsNewInstanceWithUpdatedValues() { SslBundle sslBundle = mock(SslBundle.class); - HttpClientSettings settings = new HttpClientSettings(null, ONE_SECOND, null, null) - .orElse(new HttpClientSettings(HttpRedirects.FOLLOW_WHEN_POSSIBLE, TWO_SECONDS, TWO_SECONDS, sslBundle)); + HttpClientSettings settings = new HttpClientSettings(null, null, ONE_SECOND, null, null) + .orElse(new HttpClientSettings(HttpCookies.ENABLE, HttpRedirects.FOLLOW_WHEN_POSSIBLE, TWO_SECONDS, + TWO_SECONDS, sslBundle)); + assertThat(settings.cookies()).isEqualTo(HttpCookies.ENABLE); assertThat(settings.redirects()).isEqualTo(HttpRedirects.FOLLOW_WHEN_POSSIBLE); assertThat(settings.connectTimeout()).isEqualTo(ONE_SECOND); assertThat(settings.readTimeout()).isEqualTo(TWO_SECONDS); 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 f3ebb359d0eb..9bc5844f677c 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 @@ -52,14 +52,14 @@ void createsHttpClientSettingsFromProperties() { .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( - HttpRedirects.DONT_FOLLOW, Duration.ofSeconds(1), Duration.ofSeconds(2), null))); + null, HttpRedirects.DONT_FOLLOW, Duration.ofSeconds(1), Duration.ofSeconds(2), null))); } @Test void doesNotReplaceUserProvidedHttpClientSettings() { this.contextRunner.withUserConfiguration(TestHttpClientConfiguration.class) .run((context) -> assertThat(context.getBean(HttpClientSettings.class)) - .isEqualTo(new HttpClientSettings(null, Duration.ofSeconds(1), Duration.ofSeconds(2), null))); + .isEqualTo(new HttpClientSettings(null, null, Duration.ofSeconds(1), Duration.ofSeconds(2), null))); } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientSettingsPropertyMapperTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientSettingsPropertyMapperTests.java index 2a49cce16f4d..e6d0b8b77829 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientSettingsPropertyMapperTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientSettingsPropertyMapperTests.java @@ -93,7 +93,7 @@ void mapMapsSslBundle() { @Test void mapUsesBaseSettingsForMissingProperties() { - HttpClientSettings baseSettings = new HttpClientSettings(HttpRedirects.FOLLOW_WHEN_POSSIBLE, + HttpClientSettings baseSettings = new HttpClientSettings(null, HttpRedirects.FOLLOW_WHEN_POSSIBLE, Duration.ofSeconds(15), Duration.ofSeconds(25), null); HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(null, baseSettings); TestHttpClientSettingsProperties properties = new TestHttpClientSettingsProperties(); diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java index d39a04aa96d2..a9afb4121ca9 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java @@ -35,6 +35,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.HttpCookies; import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.ssl.SslBundle; import org.springframework.http.client.ClientHttpRequest; @@ -458,6 +459,20 @@ public RestTemplateBuilder readTimeout(Duration readTimeout) { this.customizers, this.requestCustomizers); } + /** + * Sets the cookie handling strategy on the underlying + * {@link ClientHttpRequestFactory}. + * @param cookies the cookie handling strategy + * @return a new builder instance. + * @since 4.1.0 + */ + public RestTemplateBuilder cookies(HttpCookies cookies) { + return new RestTemplateBuilder(this.clientSettings.withCookies(cookies), this.detectRequestFactory, + this.rootUri, this.messageConverters, this.interceptors, this.requestFactoryBuilder, + this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, + this.customizers, this.requestCustomizers); + } + /** * Sets the redirect strategy on the underlying {@link ClientHttpRequestFactory}. * @param redirects the redirect strategy diff --git a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java index 48118a3680e0..c446073a4e68 100644 --- a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java +++ b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java @@ -41,6 +41,7 @@ import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpComponentsClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.HttpComponentsHttpClientBuilder.TlsSocketStrategyFactory; +import org.springframework.boot.http.client.HttpCookies; import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.boot.restclient.RootUriTemplateHandler; @@ -70,8 +71,7 @@ * status code}. *
* A {@code TestRestTemplate} can optionally carry Basic authentication headers. If Apache - * Http Client 4.3.2 or better is available (recommended) it will be used as the client, - * and by default configured to ignore cookies. + * Http Client 4.3.2 or better is available (recommended) it will be used as the client. *
* Note: To prevent injection problems this class intentionally does not extend * {@link RestTemplate}. If you need access to the underlying {@link RestTemplate} use @@ -161,10 +161,13 @@ private static RestTemplateBuilder createInitialBuilder(RestTemplateBuilder buil return builder; } + @SuppressWarnings("deprecation") private static HttpComponentsClientHttpRequestFactoryBuilder applyHttpClientOptions( HttpComponentsClientHttpRequestFactoryBuilder builder, HttpClientOption[] httpClientOptions) { - builder = builder.withDefaultRequestConfigCustomizer( - new CookieSpecCustomizer(HttpClientOption.ENABLE_COOKIES.isPresent(httpClientOptions))); + if (HttpClientOption.ENABLE_COOKIES.isPresent(httpClientOptions)) { + builder = builder.withDefaultRequestConfigCustomizer( + new CookieSpecCustomizer(true)); + } if (HttpClientOption.SSL.isPresent(httpClientOptions)) { builder = builder.withTlsSocketStrategyFactory(new SelfSignedTlsSocketStrategyFactory()); } @@ -974,6 +977,19 @@ public TestRestTemplate withRedirects(HttpRedirects redirects) { return withClientSettings((settings) -> settings.withRedirects(redirects)); } + /** + * Creates a new {@code TestRestTemplate} with the same configuration as this one, + * except that it will apply the given {@link HttpCookies}. The request factory used is + * a new instance of the underlying {@link RestTemplate}'s request factory type (when + * possible). + * @param cookies the new cookie settings + * @return the new template + * @since 4.1.0 + */ + public TestRestTemplate withCookies(HttpCookies cookies) { + return withClientSettings((settings) -> settings.withCookies(cookies)); + } + /** * Creates a new {@code TestRestTemplate} with the same configuration as this one, * except that it will apply the given {@link HttpClientSettings}. The request factory @@ -1036,7 +1052,10 @@ public enum HttpClientOption { /** * Enable cookies. + * @deprecated since 4.1.0 for removal in 5.0.0 in favor of + * {@link TestRestTemplate#withCookies(HttpCookies)} */ + @Deprecated(since = "4.1.0", forRemoval = true) ENABLE_COOKIES, /** diff --git a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java index c59889e850ab..977c456ae451 100644 --- a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java +++ b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java @@ -35,6 +35,7 @@ import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.HttpCookies; import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.boot.resttestclient.TestRestTemplate.HttpClientOption; @@ -140,11 +141,26 @@ void authenticated() { } @Test + @SuppressWarnings("removal") void options() { RequestConfig config = getRequestConfig(new TestRestTemplate(HttpClientOption.ENABLE_COOKIES)); assertThat(config.getCookieSpec()).isEqualTo("strict"); } + @Test + void defaultCookieSpecMatchesRestTemplate() { + RequestConfig config = getRequestConfig(new TestRestTemplate()); + assertThat(config.getCookieSpec()).isNull(); + } + + @Test + void withCookies() { + TestRestTemplate template = new TestRestTemplate(); + assertThat(getRequestConfig(template).getCookieSpec()).isNull(); + assertThat(getRequestConfig(template.withCookies(HttpCookies.ENABLE)).getCookieSpec()).isEqualTo("strict"); + assertThat(getRequestConfig(template.withCookies(HttpCookies.DISABLE)).getCookieSpec()).isEqualTo("ignoreCookies"); + } + @Test void jdkBuilderCanBeSpecifiedWithSpecificRedirects() { RestTemplateBuilder builder = new RestTemplateBuilder()