Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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
* <a href="https://hc.apache.org/httpcomponents-client-ga/">Apache HttpComponents</a>
* 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;
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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

}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -70,8 +71,7 @@
* status code}.
* <p>
* 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.
* <p>
* Note: To prevent injection problems this class intentionally does not extend
* {@link RestTemplate}. If you need access to the underlying {@link RestTemplate} use
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down