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,36 @@
* @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);
/**
* Create a new {@link HttpClientSettings} instance.
* @param redirects the follow redirect strategy to use
* @param connectTimeout the connect timeout
* @param readTimeout the read timeout
* @param sslBundle the SSL bundle providing SSL configuration
* @deprecated since 4.1.0 for removal in 4.3.0 in favor of
* {@link HttpClientSettings#HttpClientSettings(HttpCookies, HttpRedirects, Duration, Duration, SslBundle)}
*/
@Deprecated(since = "4.1.0", forRemoval = true)
public HttpClientSettings(@Nullable HttpRedirects redirects, @Nullable Duration connectTimeout,
@Nullable Duration readTimeout, @Nullable SslBundle sslBundle) {
this(null, redirects, connectTimeout, readTimeout, sslBundle);
}

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 +74,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 +85,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 +97,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 +108,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 +118,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 +132,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 @@ -16,6 +16,8 @@

package org.springframework.boot.http.client;

import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Redirect;
import java.util.concurrent.Executor;
Expand Down Expand Up @@ -83,6 +85,7 @@ public HttpClient build(@Nullable HttpClientSettings settings) {
Assert.isTrue(settings.readTimeout() == null, "'settings' must not have a 'readTimeout'");
HttpClient.Builder builder = HttpClient.newBuilder();
PropertyMapper map = PropertyMapper.get();
map.from(settings::cookies).as(this::asCookieHandler).to(builder::cookieHandler);
map.from(settings::redirects).always().as(this::asHttpClientRedirect).to(builder::followRedirects);
map.from(settings::connectTimeout).to(builder::connectTimeout);
map.from(settings::sslBundle).as(SslBundle::createSslContext).to(builder::sslContext);
Expand All @@ -99,6 +102,13 @@ private SSLParameters asSslParameters(SslBundle sslBundle) {
return parameters;
}

private @Nullable CookieHandler asCookieHandler(HttpCookies cookies) {
return switch (cookies) {
case ENABLE_WHEN_POSSIBLE, ENABLE -> new CookieManager();
case DISABLE -> null;
};
}

private Redirect asHttpClientRedirect(@Nullable HttpRedirects redirects) {
if (redirects == null) {
return Redirect.NORMAL;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.transport.HttpClientTransportDynamic;
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
import org.eclipse.jetty.http.HttpCookieStore;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.jspecify.annotations.Nullable;
Expand Down Expand Up @@ -140,6 +141,7 @@ public HttpClient build(@Nullable HttpClientSettings settings) {
HttpClient httpClient = createHttpClient(settings.readTimeout(), transport);
PropertyMapper map = PropertyMapper.get();
map.from(settings::connectTimeout).as(Duration::toMillis).to(httpClient::setConnectTimeout);
map.from(settings::cookies).as(this::asCookieStore).to(httpClient::setHttpCookieStore);
map.from(settings::redirects).always().as(this::followRedirects).to(httpClient::setFollowRedirects);
this.customizer.accept(httpClient);
return httpClient;
Expand Down Expand Up @@ -182,6 +184,13 @@ private SslContextFactory.Client createSslContextFactory(SslBundle sslBundle) {
return factory;
}

private @Nullable HttpCookieStore asCookieStore(HttpCookies cookies) {
return switch (cookies) {
case ENABLE_WHEN_POSSIBLE, ENABLE -> null;
case DISABLE -> new HttpCookieStore.Empty();
};
}

private boolean followRedirects(@Nullable HttpRedirects redirects) {
if (redirects == null) {
return true;
Expand Down
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
Loading