Unify HTTP client redirect behavior and provide configuration option

Update `ClientHttpRequestFactoryBuilder` implementations to ensure
that all libraries have consistent redirect follow behavior. Following
of redirects is enabled by default.

The `ClientHttpRequestFactorySettings` may be used to change if
redirects should be followed. The `spring.http.client.redirects`
property may also be used to update the default behavior.

Closes gh-42879
This commit is contained in:
Phillip Webb 2024-10-25 00:46:01 -07:00
parent a92001130f
commit 36a22fcd59
20 changed files with 307 additions and 74 deletions

View File

@ -58,8 +58,8 @@ public class HttpClientAutoConfiguration {
ClientHttpRequestFactorySettings clientHttpRequestFactorySettings(HttpClientProperties httpClientProperties,
ObjectProvider<SslBundles> sslBundles) {
SslBundle sslBundle = getSslBundle(httpClientProperties.getSsl(), sslBundles);
return new ClientHttpRequestFactorySettings(httpClientProperties.getConnectTimeout(),
httpClientProperties.getReadTimeout(), sslBundle);
return new ClientHttpRequestFactorySettings(httpClientProperties.getRedirects(),
httpClientProperties.getConnectTimeout(), httpClientProperties.getReadTimeout(), sslBundle);
}
private SslBundle getSslBundle(HttpClientProperties.Ssl properties, ObjectProvider<SslBundles> sslBundles) {

View File

@ -21,6 +21,7 @@ import java.util.function.Supplier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
/**
* {@link ConfigurationProperties @ConfigurationProperties} for a Spring's blocking HTTP
@ -37,6 +38,11 @@ public class HttpClientProperties {
*/
private Factory factory;
/**
* Handling for HTTP redirects.
*/
private Redirects redirects = Redirects.FOLLOW_WHEN_POSSIBLE;
/**
* Default connect timeout for a client HTTP request.
*/
@ -60,6 +66,14 @@ public class HttpClientProperties {
this.factory = factory;
}
public Redirects getRedirects() {
return this.redirects;
}
public void setRedirects(Redirects redirects) {
this.redirects = redirects;
}
public Duration getConnectTimeout() {
return this.connectTimeout;
}

View File

@ -26,6 +26,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
import org.springframework.boot.http.client.SimpleClientHttpRequestFactoryBuilder;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
@ -60,10 +61,11 @@ class HttpClientAutoConfigurationTests {
@Test
void configuresClientHttpRequestFactorySettings() {
this.contextRunner.withPropertyValues(sslPropertyValues().toArray(String[]::new))
.withPropertyValues("spring.http.client.connect-timeout=10s", "spring.http.client.read-timeout=20s",
"spring.http.client.ssl.bundle=test")
.withPropertyValues("spring.http.client.redirects=dont-follow", "spring.http.client.connect-timeout=10s",
"spring.http.client.read-timeout=20s", "spring.http.client.ssl.bundle=test")
.run((context) -> {
ClientHttpRequestFactorySettings settings = context.getBean(ClientHttpRequestFactorySettings.class);
assertThat(settings.redirects()).isEqualTo(Redirects.DONT_FOLLOW);
assertThat(settings.connectTimeout()).isEqualTo(Duration.ofSeconds(10));
assertThat(settings.readTimeout()).isEqualTo(Duration.ofSeconds(20));
assertThat(settings.sslBundle().getKey().getAlias()).isEqualTo("alias1");

View File

@ -1018,7 +1018,7 @@ public class TestRestTemplate {
@SuppressWarnings("removal")
public CustomHttpComponentsClientHttpRequestFactory(HttpClientOption[] httpClientOptions,
org.springframework.boot.web.client.ClientHttpRequestFactorySettings settings) {
this(httpClientOptions, new ClientHttpRequestFactorySettings(settings.connectTimeout(),
this(httpClientOptions, new ClientHttpRequestFactorySettings(null, settings.connectTimeout(),
settings.readTimeout(), settings.sslBundle()));
}

View File

@ -24,6 +24,8 @@ import org.springframework.http.client.ClientHttpRequestFactory;
/**
* Settings that can be applied when creating a {@link ClientHttpRequestFactory}.
*
* @param redirects the follow redirect strategy to use or null to redirect whenever the
* underlying library allows it
* @param connectTimeout the connect timeout
* @param readTimeout the read timeout
* @param sslBundle the SSL bundle providing SSL configuration
@ -33,10 +35,15 @@ import org.springframework.http.client.ClientHttpRequestFactory;
* @since 3.4.0
* @see ClientHttpRequestFactoryBuilder
*/
public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration readTimeout, SslBundle sslBundle) {
public record ClientHttpRequestFactorySettings(Redirects redirects, Duration connectTimeout, Duration readTimeout,
SslBundle sslBundle) {
private static final ClientHttpRequestFactorySettings defaults = new ClientHttpRequestFactorySettings(null, null,
null);
null, null);
public ClientHttpRequestFactorySettings {
redirects = (redirects != null) ? redirects : Redirects.FOLLOW_WHEN_POSSIBLE;
}
/**
* Return a new {@link ClientHttpRequestFactorySettings} instance with an updated
@ -45,7 +52,7 @@ public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration
* @return a new {@link ClientHttpRequestFactorySettings} instance
*/
public ClientHttpRequestFactorySettings withConnectTimeout(Duration connectTimeout) {
return new ClientHttpRequestFactorySettings(connectTimeout, this.readTimeout, this.sslBundle);
return new ClientHttpRequestFactorySettings(this.redirects, connectTimeout, this.readTimeout, this.sslBundle);
}
/**
@ -56,7 +63,7 @@ public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration
*/
public ClientHttpRequestFactorySettings withReadTimeout(Duration readTimeout) {
return new ClientHttpRequestFactorySettings(this.connectTimeout, readTimeout, this.sslBundle);
return new ClientHttpRequestFactorySettings(this.redirects, this.connectTimeout, readTimeout, this.sslBundle);
}
/**
@ -66,7 +73,17 @@ public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration
* @return a new {@link ClientHttpRequestFactorySettings} instance
*/
public ClientHttpRequestFactorySettings withSslBundle(SslBundle sslBundle) {
return new ClientHttpRequestFactorySettings(this.connectTimeout, this.readTimeout, sslBundle);
return new ClientHttpRequestFactorySettings(this.redirects, this.connectTimeout, this.readTimeout, sslBundle);
}
/**
* Return a new {@link ClientHttpRequestFactorySettings} instance with an updated
* redirect setting.
* @param redirects the new redirects setting
* @return a new {@link ClientHttpRequestFactorySettings} instance
*/
public ClientHttpRequestFactorySettings withRedirects(Redirects redirects) {
return new ClientHttpRequestFactorySettings(redirects, this.connectTimeout, this.readTimeout, this.sslBundle);
}
/**
@ -88,4 +105,26 @@ public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration
return defaults;
}
/**
* Redirect strategies.
*/
public enum Redirects {
/**
* Follow redirects (if the underlying library has support).
*/
FOLLOW_WHEN_POSSIBLE,
/**
* Follow redirects (fail if the underlying library has not support).
*/
FOLLOW,
/**
* Don't follow redirects (fail if the underlying library has not support).
*/
DONT_FOLLOW
}
}

View File

@ -16,6 +16,7 @@
package org.springframework.boot.http.client;
import java.net.URI;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
@ -24,14 +25,21 @@ import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.protocol.RedirectStrategy;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslOptions;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
@ -72,31 +80,35 @@ public final class HttpComponentsClientHttpRequestFactoryBuilder
@Override
protected HttpComponentsClientHttpRequestFactory createClientHttpRequestFactory(
ClientHttpRequestFactorySettings settings) {
HttpClient httpClient = createHttpClient(settings.readTimeout(), settings.sslBundle());
HttpClient httpClient = createHttpClient(settings);
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(factory::setConnectTimeout);
return factory;
}
private HttpClient createHttpClient(Duration readTimeout, SslBundle sslBundle) {
private HttpClient createHttpClient(ClientHttpRequestFactorySettings settings) {
return HttpClientBuilder.create()
.useSystemProperties()
.setConnectionManager(createConnectionManager(readTimeout, sslBundle))
.setRedirectStrategy(asRedirectStrategy(settings.redirects()))
.setConnectionManager(createConnectionManager(settings))
.build();
}
private PoolingHttpClientConnectionManager createConnectionManager(Duration readTimeout, SslBundle sslBundle) {
PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder
.create();
if (readTimeout != null) {
connectionManagerBuilder.setDefaultSocketConfig(createSocketConfig(readTimeout));
}
if (sslBundle != null) {
connectionManagerBuilder.setTlsSocketStrategy(createTlsSocketStrategy(sslBundle));
}
PoolingHttpClientConnectionManager connectionManager = connectionManagerBuilder.useSystemProperties().build();
return connectionManager;
private RedirectStrategy asRedirectStrategy(Redirects redirects) {
return switch (redirects) {
case FOLLOW_WHEN_POSSIBLE -> DefaultRedirectStrategy.INSTANCE;
case FOLLOW -> DefaultRedirectStrategy.INSTANCE;
case DONT_FOLLOW -> NoFollowRedirectStrategy.INSTANCE;
};
}
private PoolingHttpClientConnectionManager createConnectionManager(ClientHttpRequestFactorySettings settings) {
PoolingHttpClientConnectionManagerBuilder builder = PoolingHttpClientConnectionManagerBuilder.create();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(settings::readTimeout).as(this::createSocketConfig).to(builder::setDefaultSocketConfig);
map.from(settings::sslBundle).as(this::createTlsSocketStrategy).to(builder::setTlsSocketStrategy);
return builder.useSystemProperties().build();
}
private DefaultClientTlsStrategy createTlsSocketStrategy(SslBundle sslBundle) {
@ -110,6 +122,30 @@ public final class HttpComponentsClientHttpRequestFactoryBuilder
return SocketConfig.custom().setSoTimeout((int) readTimeout.toMillis(), TimeUnit.MILLISECONDS).build();
}
/**
* {@link RedirectStrategy} that never follows redirects.
*/
private static final class NoFollowRedirectStrategy implements RedirectStrategy {
private static final RedirectStrategy INSTANCE = new NoFollowRedirectStrategy();
private NoFollowRedirectStrategy() {
}
@Override
public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context)
throws HttpException {
return false;
}
@Override
public URI getLocationURI(HttpRequest request, HttpResponse response, HttpContext context)
throws HttpException {
return null;
}
}
static class Classes {
static final String HTTP_CLIENTS = "org.apache.hc.client5.http.impl.classic.HttpClients";

View File

@ -17,12 +17,13 @@
package org.springframework.boot.http.client;
import java.net.http.HttpClient;
import java.time.Duration;
import java.net.http.HttpClient.Redirect;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.util.ClassUtils;
@ -59,24 +60,30 @@ public class JdkClientHttpRequestFactoryBuilder
@Override
protected JdkClientHttpRequestFactory createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings) {
HttpClient httpClient = createHttpClient(settings.connectTimeout(), settings.sslBundle());
HttpClient httpClient = createHttpClient(settings);
JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(httpClient);
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(settings::readTimeout).to(requestFactory::setReadTimeout);
return requestFactory;
}
private HttpClient createHttpClient(Duration connectTimeout, SslBundle sslBundle) {
private HttpClient createHttpClient(ClientHttpRequestFactorySettings settings) {
HttpClient.Builder httpClientBuilder = HttpClient.newBuilder();
if (connectTimeout != null) {
httpClientBuilder.connectTimeout(connectTimeout);
}
if (sslBundle != null) {
httpClientBuilder.sslContext(sslBundle.createSslContext());
}
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(settings::connectTimeout).to(httpClientBuilder::connectTimeout);
map.from(settings::sslBundle).as(SslBundle::createSslContext).to(httpClientBuilder::sslContext);
map.from(settings::redirects).as(this::asHttpClientRedirect).to(httpClientBuilder::followRedirects);
return httpClientBuilder.build();
}
private Redirect asHttpClientRedirect(Redirects redirects) {
return switch (redirects) {
case FOLLOW_WHEN_POSSIBLE -> Redirect.NORMAL;
case FOLLOW -> Redirect.NORMAL;
case DONT_FOLLOW -> Redirect.NEVER;
};
}
static class Classes {
static final String HTTP_CLIENT = "java.net.http.HttpClient";

View File

@ -24,11 +24,14 @@ import java.util.function.Consumer;
import javax.net.ssl.SSLContext;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpClientTransport;
import org.eclipse.jetty.client.transport.HttpClientTransportDynamic;
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.http.client.JettyClientHttpRequestFactory;
import org.springframework.util.ClassUtils;
@ -65,24 +68,44 @@ public final class JettyClientHttpRequestFactoryBuilder
@Override
protected JettyClientHttpRequestFactory createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings) {
JettyClientHttpRequestFactory requestFactory = createRequestFactory(settings.sslBundle());
JettyClientHttpRequestFactory requestFactory = createRequestFactory(settings);
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(requestFactory::setConnectTimeout);
map.from(settings::readTimeout).asInt(Duration::toMillis).to(requestFactory::setReadTimeout);
return requestFactory;
}
private static JettyClientHttpRequestFactory createRequestFactory(SslBundle sslBundle) {
if (sslBundle != null) {
SSLContext sslContext = sslBundle.createSslContext();
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
sslContextFactory.setSslContext(sslContext);
ClientConnector connector = new ClientConnector();
connector.setSslContextFactory(sslContextFactory);
HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(connector));
return new JettyClientHttpRequestFactory(httpClient);
private JettyClientHttpRequestFactory createRequestFactory(ClientHttpRequestFactorySettings settings) {
HttpClientTransport transport = createTransport(settings);
HttpClient httpClient = new HttpClient(transport);
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(settings::redirects).as(this::followRedirects).to(httpClient::setFollowRedirects);
return new JettyClientHttpRequestFactory(httpClient);
}
private HttpClientTransport createTransport(ClientHttpRequestFactorySettings settings) {
if (settings.sslBundle() == null) {
return new HttpClientTransportOverHTTP();
}
return new JettyClientHttpRequestFactory();
ClientConnector connector = createClientConnector(settings.sslBundle());
return new HttpClientTransportDynamic(connector);
}
private ClientConnector createClientConnector(SslBundle sslBundle) {
SSLContext sslContext = sslBundle.createSslContext();
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
sslContextFactory.setSslContext(sslContext);
ClientConnector connector = new ClientConnector();
connector.setSslContextFactory(sslContextFactory);
return connector;
}
private boolean followRedirects(Redirects redirects) {
return switch (redirects) {
case FOLLOW_WHEN_POSSIBLE -> true;
case FOLLOW -> true;
case DONT_FOLLOW -> false;
};
}
static class Classes {

View File

@ -28,6 +28,7 @@ import reactor.netty.http.client.HttpClient;
import reactor.netty.tcp.SslProvider.SslContextSpec;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslManagerBundle;
import org.springframework.boot.ssl.SslOptions;
@ -68,22 +69,30 @@ public final class ReactorClientHttpRequestFactoryBuilder
@Override
protected ReactorClientHttpRequestFactory createClientHttpRequestFactory(
ClientHttpRequestFactorySettings settings) {
ReactorClientHttpRequestFactory requestFactory = createRequestFactory(settings.sslBundle());
ReactorClientHttpRequestFactory requestFactory = createRequestFactory(settings);
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(requestFactory::setConnectTimeout);
map.from(settings::readTimeout).asInt(Duration::toMillis).to(requestFactory::setReadTimeout);
return requestFactory;
}
private ReactorClientHttpRequestFactory createRequestFactory(SslBundle sslBundle) {
HttpClient httpClient = HttpClient.create();
httpClient = applyDefaults(httpClient);
if (sslBundle != null) {
httpClient = httpClient.secure((ThrowingConsumer.of((spec) -> configureSsl(spec, sslBundle))));
private ReactorClientHttpRequestFactory createRequestFactory(ClientHttpRequestFactorySettings settings) {
HttpClient httpClient = applyDefaults(HttpClient.create());
httpClient = httpClient.followRedirect(followRedirects(settings.redirects()));
if (settings.sslBundle() != null) {
httpClient = httpClient.secure((ThrowingConsumer.of((spec) -> configureSsl(spec, settings.sslBundle()))));
}
return new ReactorClientHttpRequestFactory(httpClient);
}
private boolean followRedirects(Redirects redirects) {
return switch (redirects) {
case FOLLOW_WHEN_POSSIBLE -> true;
case FOLLOW -> true;
case DONT_FOLLOW -> false;
};
}
HttpClient applyDefaults(HttpClient httpClient) {
// Aligns with ReactorClientHttpRequestFactory defaults
return httpClient.compress(true);

View File

@ -23,6 +23,7 @@ import java.time.Duration;
import java.util.function.Supplier;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.util.Assert;
@ -73,7 +74,9 @@ final class ReflectiveComponentsClientHttpRequestFactoryBuilder<T extends Client
}
private void configure(ClientHttpRequestFactory requestFactory, ClientHttpRequestFactorySettings settings) {
Assert.state(settings.sslBundle() == null, "Unable to set SSL bundler using reflection");
Assert.state(settings.sslBundle() == null, "Unable to set SSL bundle using reflection");
Assert.state(settings.redirects() == Redirects.FOLLOW_WHEN_POSSIBLE,
"Unable to set redirect follow using reflection");
ClientHttpRequestFactory unwrapped = unwrapRequestFactoryIfNecessary(requestFactory);
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(settings::connectTimeout).to((connectTimeout) -> setConnectTimeout(unwrapped, connectTimeout));

View File

@ -27,6 +27,7 @@ import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.Assert;
@ -64,8 +65,7 @@ public final class SimpleClientHttpRequestFactoryBuilder
@Override
protected SimpleClientHttpRequestFactory createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings) {
SslBundle sslBundle = settings.sslBundle();
SimpleClientHttpRequestFactory requestFactory = (sslBundle != null)
? new SimpleClientHttpsRequestFactory(sslBundle) : new SimpleClientHttpRequestFactory();
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpsRequestFactory(settings);
Assert.state(sslBundle == null || !sslBundle.getOptions().isSpecified(),
"SSL Options cannot be specified with Java connections");
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
@ -75,23 +75,27 @@ public final class SimpleClientHttpRequestFactoryBuilder
}
/**
* {@link SimpleClientHttpsRequestFactory} to configure SSL from an {@link SslBundle}.
* {@link SimpleClientHttpsRequestFactory} to configure SSL from an {@link SslBundle}
* and {@link Redirects}.
*/
private static class SimpleClientHttpsRequestFactory extends SimpleClientHttpRequestFactory {
private final SslBundle sslBundle;
private final ClientHttpRequestFactorySettings settings;
SimpleClientHttpsRequestFactory(SslBundle sslBundle) {
this.sslBundle = sslBundle;
SimpleClientHttpsRequestFactory(ClientHttpRequestFactorySettings settings) {
this.settings = settings;
}
@Override
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
super.prepareConnection(connection, httpMethod);
if (this.sslBundle != null && connection instanceof HttpsURLConnection secureConnection) {
SSLSocketFactory socketFactory = this.sslBundle.createSslContext().getSocketFactory();
if (this.settings.sslBundle() != null && connection instanceof HttpsURLConnection secureConnection) {
SSLSocketFactory socketFactory = this.settings.sslBundle().createSslContext().getSocketFactory();
secureConnection.setSSLSocketFactory(socketFactory);
}
if (this.settings.redirects() == Redirects.DONT_FOLLOW) {
connection.setInstanceFollowRedirects(false);
}
}
}

View File

@ -79,7 +79,7 @@ public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration
}
org.springframework.boot.http.client.ClientHttpRequestFactorySettings adapt() {
return new org.springframework.boot.http.client.ClientHttpRequestFactorySettings(connectTimeout(),
return new org.springframework.boot.http.client.ClientHttpRequestFactorySettings(null, connectTimeout(),
readTimeout(), sslBundle());
}

View File

@ -18,6 +18,7 @@ package org.springframework.boot.http.client;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
@ -31,6 +32,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleKey;
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
@ -41,8 +43,10 @@ import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.Ssl.ClientAuth;
import org.springframework.boot.web.server.WebServer;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
@ -116,6 +120,45 @@ abstract class AbstractClientHttpRequestFactoryBuilderTests<T extends ClientHttp
}
}
@Test
void redirectDefault() throws Exception {
testRedirect(null, HttpStatus.OK);
}
@Test
void redirectFollow() throws Exception {
testRedirect(ClientHttpRequestFactorySettings.defaults().withRedirects(Redirects.FOLLOW), HttpStatus.OK);
}
@Test
void redirectDontFollow() throws Exception {
testRedirect(ClientHttpRequestFactorySettings.defaults().withRedirects(Redirects.DONT_FOLLOW),
HttpStatus.FOUND);
}
private void testRedirect(ClientHttpRequestFactorySettings settings, HttpStatus expectedStatus)
throws URISyntaxException, IOException {
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0);
WebServer webServer = webServerFactory
.getWebServer((context) -> context.addServlet("test", TestServlet.class).addMapping("/"));
try {
webServer.start();
int port = webServer.getPort();
URI uri = new URI("http://localhost:%s".formatted(port) + "/redirect");
ClientHttpRequestFactory requestFactory = this.builder.build(settings);
ClientHttpRequest request = requestFactory.createRequest(uri, HttpMethod.GET);
ClientHttpResponse response = request.execute();
assertThat(response.getStatusCode()).isEqualTo(expectedStatus);
if (expectedStatus == HttpStatus.OK) {
assertThat(response.getBody()).asString(StandardCharsets.UTF_8)
.contains("Received GET request to /redirected");
}
}
finally {
webServer.stop();
}
}
private ClientHttpRequest request(ClientHttpRequestFactory factory, URI uri, String method) throws IOException {
return factory.createRequest(uri, HttpMethod.valueOf(method));
}
@ -143,6 +186,10 @@ abstract class AbstractClientHttpRequestFactoryBuilderTests<T extends ClientHttp
@Override
public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
if ("/redirect".equals(req.getRequestURI())) {
res.sendRedirect("/redirected");
return;
}
res.getWriter().println("Received " + req.getMethod() + " request to " + req.getRequestURI());
}

View File

@ -20,6 +20,7 @@ import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
import org.springframework.boot.ssl.SslBundle;
import static org.assertj.core.api.Assertions.assertThat;
@ -35,8 +36,18 @@ class ClientHttpRequestFactorySettingsTests {
private static final Duration ONE_SECOND = Duration.ofSeconds(1);
@Test
void defaultsHasNullValues() {
void defaults() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults();
assertThat(settings.redirects()).isEqualTo(Redirects.FOLLOW_WHEN_POSSIBLE);
assertThat(settings.connectTimeout()).isNull();
assertThat(settings.readTimeout()).isNull();
assertThat(settings.sslBundle()).isNull();
}
@Test
void createWithNullsUsesDefaults() {
ClientHttpRequestFactorySettings settings = new ClientHttpRequestFactorySettings(null, null, null, null);
assertThat(settings.redirects()).isEqualTo(Redirects.FOLLOW_WHEN_POSSIBLE);
assertThat(settings.connectTimeout()).isNull();
assertThat(settings.readTimeout()).isNull();
assertThat(settings.sslBundle()).isNull();
@ -70,4 +81,11 @@ class ClientHttpRequestFactorySettingsTests {
assertThat(settings.sslBundle()).isSameAs(sslBundle);
}
@Test
void withRedirectsReturnsInstanceWithUpdatedRedirect() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withRedirects(Redirects.DONT_FOLLOW);
assertThat(settings.redirects()).isEqualTo(Redirects.DONT_FOLLOW);
}
}

View File

@ -22,6 +22,7 @@ import java.time.Duration;
import org.eclipse.jetty.client.HttpClient;
import org.junit.jupiter.api.Test;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequest;
@ -52,6 +53,22 @@ class ReflectiveComponentsClientHttpRequestFactoryBuilderTests
.withMessage("Unable to set SSL bundler using reflection");
}
@Override
void redirectFollow() throws Exception {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withRedirects(Redirects.FOLLOW);
assertThatIllegalStateException().isThrownBy(() -> ofTestRequestFactory().build(settings))
.withMessage("Unable to set redirect follow using reflection");
}
@Override
void redirectDontFollow() throws Exception {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withRedirects(Redirects.DONT_FOLLOW);
assertThatIllegalStateException().isThrownBy(() -> ofTestRequestFactory().build(settings))
.withMessage("Unable to set redirect follow using reflection");
}
@Test
void buildWithClassCreatesFactory() {
assertThat(ofTestRequestFactory().build()).isInstanceOf(TestClientHttpRequestFactory.class);

View File

@ -36,8 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Dave Syer
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = { "management.server.port:0", "spring.http.client.factory=simple" })
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "management.server.port:0" })
class SampleActuatorUiApplicationPortTests {
@LocalServerPort

View File

@ -39,8 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Dave Syer
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = { "server.error.include-message=always", "spring.http.client.factory=simple" })
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "server.error.include-message=always" })
class SampleActuatorUiApplicationTests {
@Autowired
@ -51,7 +50,7 @@ class SampleActuatorUiApplicationTests {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.TEXT_HTML));
ResponseEntity<String> entity = this.restTemplate.withBasicAuth("user", getPassword())
.exchange("/", HttpMethod.GET, new HttpEntity<>(headers), String.class);
.exchange("/", HttpMethod.GET, new HttpEntity<Void>(headers), String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).contains("<title>Hello");
}
@ -75,7 +74,7 @@ class SampleActuatorUiApplicationTests {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.TEXT_HTML));
ResponseEntity<String> entity = this.restTemplate.withBasicAuth("user", getPassword())
.exchange("/error", HttpMethod.GET, new HttpEntity<>(headers), String.class);
.exchange("/error", HttpMethod.GET, new HttpEntity<Void>(headers), String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
assertThat(entity.getBody()).contains("<html>")
.contains("<body>")

View File

@ -25,8 +25,12 @@ import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
@ -37,6 +41,7 @@ import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
@ -48,12 +53,22 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Madhura Bhave
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = { "server.servlet.session.timeout:2", "spring.http.client.factory=simple" })
properties = { "server.servlet.session.timeout:2" })
class SampleSessionJdbcApplicationTests {
private static final ClientHttpRequestFactorySettings DONT_FOLLOW_REDIRECTS = ClientHttpRequestFactorySettings
.defaults()
.withRedirects(Redirects.DONT_FOLLOW);
@Autowired
private RestTemplateBuilder restTemplateBuilder;
@Autowired
private TestRestTemplate restTemplate;
@LocalServerPort
private String port;
private static final URI ROOT_URI = URI.create("/");
@Test
@ -68,14 +83,15 @@ class SampleSessionJdbcApplicationTests {
}
private String performLogin() {
RestTemplate restTemplate = this.restTemplateBuilder.requestFactorySettings(DONT_FOLLOW_REDIRECTS).build();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML));
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.set("username", "user");
form.set("password", "password");
ResponseEntity<String> entity = this.restTemplate.exchange("/login", HttpMethod.POST,
new HttpEntity<>(form, headers), String.class);
ResponseEntity<String> entity = restTemplate.exchange("http://localhost:" + this.port + "/login",
HttpMethod.POST, new HttpEntity<>(form, headers), String.class);
return entity.getHeaders().getFirst("Set-Cookie");
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2024 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.
@ -37,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Dave Syer
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.http.client.redirects=dont-follow")
class SampleGroovyTemplateApplicationTests {
@LocalServerPort

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2024 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.
@ -37,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Dave Syer
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.http.client.redirects=dont-follow")
class SampleWebUiApplicationTests {
@Autowired