Skip to content

Commit 36a22fc

Browse files
committed
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
1 parent a920011 commit 36a22fc

File tree

20 files changed

+307
-74
lines changed

20 files changed

+307
-74
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/client/HttpClientAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ ClientHttpRequestFactoryBuilder<?> clientHttpRequestFactoryBuilder(HttpClientPro
5858
ClientHttpRequestFactorySettings clientHttpRequestFactorySettings(HttpClientProperties httpClientProperties,
5959
ObjectProvider<SslBundles> sslBundles) {
6060
SslBundle sslBundle = getSslBundle(httpClientProperties.getSsl(), sslBundles);
61-
return new ClientHttpRequestFactorySettings(httpClientProperties.getConnectTimeout(),
62-
httpClientProperties.getReadTimeout(), sslBundle);
61+
return new ClientHttpRequestFactorySettings(httpClientProperties.getRedirects(),
62+
httpClientProperties.getConnectTimeout(), httpClientProperties.getReadTimeout(), sslBundle);
6363
}
6464

6565
private SslBundle getSslBundle(HttpClientProperties.Ssl properties, ObjectProvider<SslBundles> sslBundles) {

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/client/HttpClientProperties.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.springframework.boot.context.properties.ConfigurationProperties;
2323
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
24+
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
2425

2526
/**
2627
* {@link ConfigurationProperties @ConfigurationProperties} for a Spring's blocking HTTP
@@ -37,6 +38,11 @@ public class HttpClientProperties {
3738
*/
3839
private Factory factory;
3940

41+
/**
42+
* Handling for HTTP redirects.
43+
*/
44+
private Redirects redirects = Redirects.FOLLOW_WHEN_POSSIBLE;
45+
4046
/**
4147
* Default connect timeout for a client HTTP request.
4248
*/
@@ -60,6 +66,14 @@ public void setFactory(Factory factory) {
6066
this.factory = factory;
6167
}
6268

69+
public Redirects getRedirects() {
70+
return this.redirects;
71+
}
72+
73+
public void setRedirects(Redirects redirects) {
74+
this.redirects = redirects;
75+
}
76+
6377
public Duration getConnectTimeout() {
6478
return this.connectTimeout;
6579
}

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/client/HttpClientAutoConfigurationTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
2727
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
2828
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
29+
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
2930
import org.springframework.boot.http.client.SimpleClientHttpRequestFactoryBuilder;
3031
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
3132
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
@@ -60,10 +61,11 @@ void configuresDefinedClientHttpRequestFactoryBuilder() {
6061
@Test
6162
void configuresClientHttpRequestFactorySettings() {
6263
this.contextRunner.withPropertyValues(sslPropertyValues().toArray(String[]::new))
63-
.withPropertyValues("spring.http.client.connect-timeout=10s", "spring.http.client.read-timeout=20s",
64-
"spring.http.client.ssl.bundle=test")
64+
.withPropertyValues("spring.http.client.redirects=dont-follow", "spring.http.client.connect-timeout=10s",
65+
"spring.http.client.read-timeout=20s", "spring.http.client.ssl.bundle=test")
6566
.run((context) -> {
6667
ClientHttpRequestFactorySettings settings = context.getBean(ClientHttpRequestFactorySettings.class);
68+
assertThat(settings.redirects()).isEqualTo(Redirects.DONT_FOLLOW);
6769
assertThat(settings.connectTimeout()).isEqualTo(Duration.ofSeconds(10));
6870
assertThat(settings.readTimeout()).isEqualTo(Duration.ofSeconds(20));
6971
assertThat(settings.sslBundle().getKey().getAlias()).isEqualTo("alias1");

spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1018,7 +1018,7 @@ protected static class CustomHttpComponentsClientHttpRequestFactory extends Http
10181018
@SuppressWarnings("removal")
10191019
public CustomHttpComponentsClientHttpRequestFactory(HttpClientOption[] httpClientOptions,
10201020
org.springframework.boot.web.client.ClientHttpRequestFactorySettings settings) {
1021-
this(httpClientOptions, new ClientHttpRequestFactorySettings(settings.connectTimeout(),
1021+
this(httpClientOptions, new ClientHttpRequestFactorySettings(null, settings.connectTimeout(),
10221022
settings.readTimeout(), settings.sslBundle()));
10231023
}
10241024

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ClientHttpRequestFactorySettings.java

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
/**
2525
* Settings that can be applied when creating a {@link ClientHttpRequestFactory}.
2626
*
27+
* @param redirects the follow redirect strategy to use or null to redirect whenever the
28+
* underlying library allows it
2729
* @param connectTimeout the connect timeout
2830
* @param readTimeout the read timeout
2931
* @param sslBundle the SSL bundle providing SSL configuration
@@ -33,10 +35,15 @@
3335
* @since 3.4.0
3436
* @see ClientHttpRequestFactoryBuilder
3537
*/
36-
public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration readTimeout, SslBundle sslBundle) {
38+
public record ClientHttpRequestFactorySettings(Redirects redirects, Duration connectTimeout, Duration readTimeout,
39+
SslBundle sslBundle) {
3740

3841
private static final ClientHttpRequestFactorySettings defaults = new ClientHttpRequestFactorySettings(null, null,
39-
null);
42+
null, null);
43+
44+
public ClientHttpRequestFactorySettings {
45+
redirects = (redirects != null) ? redirects : Redirects.FOLLOW_WHEN_POSSIBLE;
46+
}
4047

4148
/**
4249
* Return a new {@link ClientHttpRequestFactorySettings} instance with an updated
@@ -45,7 +52,7 @@ public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration
4552
* @return a new {@link ClientHttpRequestFactorySettings} instance
4653
*/
4754
public ClientHttpRequestFactorySettings withConnectTimeout(Duration connectTimeout) {
48-
return new ClientHttpRequestFactorySettings(connectTimeout, this.readTimeout, this.sslBundle);
55+
return new ClientHttpRequestFactorySettings(this.redirects, connectTimeout, this.readTimeout, this.sslBundle);
4956
}
5057

5158
/**
@@ -56,7 +63,7 @@ public ClientHttpRequestFactorySettings withConnectTimeout(Duration connectTimeo
5663
*/
5764

5865
public ClientHttpRequestFactorySettings withReadTimeout(Duration readTimeout) {
59-
return new ClientHttpRequestFactorySettings(this.connectTimeout, readTimeout, this.sslBundle);
66+
return new ClientHttpRequestFactorySettings(this.redirects, this.connectTimeout, readTimeout, this.sslBundle);
6067
}
6168

6269
/**
@@ -66,7 +73,17 @@ public ClientHttpRequestFactorySettings withReadTimeout(Duration readTimeout) {
6673
* @return a new {@link ClientHttpRequestFactorySettings} instance
6774
*/
6875
public ClientHttpRequestFactorySettings withSslBundle(SslBundle sslBundle) {
69-
return new ClientHttpRequestFactorySettings(this.connectTimeout, this.readTimeout, sslBundle);
76+
return new ClientHttpRequestFactorySettings(this.redirects, this.connectTimeout, this.readTimeout, sslBundle);
77+
}
78+
79+
/**
80+
* Return a new {@link ClientHttpRequestFactorySettings} instance with an updated
81+
* redirect setting.
82+
* @param redirects the new redirects setting
83+
* @return a new {@link ClientHttpRequestFactorySettings} instance
84+
*/
85+
public ClientHttpRequestFactorySettings withRedirects(Redirects redirects) {
86+
return new ClientHttpRequestFactorySettings(redirects, this.connectTimeout, this.readTimeout, this.sslBundle);
7087
}
7188

7289
/**
@@ -88,4 +105,26 @@ public static ClientHttpRequestFactorySettings defaults() {
88105
return defaults;
89106
}
90107

108+
/**
109+
* Redirect strategies.
110+
*/
111+
public enum Redirects {
112+
113+
/**
114+
* Follow redirects (if the underlying library has support).
115+
*/
116+
FOLLOW_WHEN_POSSIBLE,
117+
118+
/**
119+
* Follow redirects (fail if the underlying library has not support).
120+
*/
121+
FOLLOW,
122+
123+
/**
124+
* Don't follow redirects (fail if the underlying library has not support).
125+
*/
126+
DONT_FOLLOW
127+
128+
}
129+
91130
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilder.java

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.http.client;
1818

19+
import java.net.URI;
1920
import java.time.Duration;
2021
import java.util.Collection;
2122
import java.util.Collections;
@@ -24,14 +25,21 @@
2425
import java.util.function.Consumer;
2526

2627
import org.apache.hc.client5.http.classic.HttpClient;
28+
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
2729
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
2830
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
2931
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
32+
import org.apache.hc.client5.http.protocol.RedirectStrategy;
3033
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
3134
import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
35+
import org.apache.hc.core5.http.HttpException;
36+
import org.apache.hc.core5.http.HttpRequest;
37+
import org.apache.hc.core5.http.HttpResponse;
3238
import org.apache.hc.core5.http.io.SocketConfig;
39+
import org.apache.hc.core5.http.protocol.HttpContext;
3340

3441
import org.springframework.boot.context.properties.PropertyMapper;
42+
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
3543
import org.springframework.boot.ssl.SslBundle;
3644
import org.springframework.boot.ssl.SslOptions;
3745
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
@@ -72,31 +80,35 @@ public HttpComponentsClientHttpRequestFactoryBuilder withCustomizers(
7280
@Override
7381
protected HttpComponentsClientHttpRequestFactory createClientHttpRequestFactory(
7482
ClientHttpRequestFactorySettings settings) {
75-
HttpClient httpClient = createHttpClient(settings.readTimeout(), settings.sslBundle());
83+
HttpClient httpClient = createHttpClient(settings);
7684
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
7785
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
7886
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(factory::setConnectTimeout);
7987
return factory;
8088
}
8189

82-
private HttpClient createHttpClient(Duration readTimeout, SslBundle sslBundle) {
90+
private HttpClient createHttpClient(ClientHttpRequestFactorySettings settings) {
8391
return HttpClientBuilder.create()
8492
.useSystemProperties()
85-
.setConnectionManager(createConnectionManager(readTimeout, sslBundle))
93+
.setRedirectStrategy(asRedirectStrategy(settings.redirects()))
94+
.setConnectionManager(createConnectionManager(settings))
8695
.build();
8796
}
8897

89-
private PoolingHttpClientConnectionManager createConnectionManager(Duration readTimeout, SslBundle sslBundle) {
90-
PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder
91-
.create();
92-
if (readTimeout != null) {
93-
connectionManagerBuilder.setDefaultSocketConfig(createSocketConfig(readTimeout));
94-
}
95-
if (sslBundle != null) {
96-
connectionManagerBuilder.setTlsSocketStrategy(createTlsSocketStrategy(sslBundle));
97-
}
98-
PoolingHttpClientConnectionManager connectionManager = connectionManagerBuilder.useSystemProperties().build();
99-
return connectionManager;
98+
private RedirectStrategy asRedirectStrategy(Redirects redirects) {
99+
return switch (redirects) {
100+
case FOLLOW_WHEN_POSSIBLE -> DefaultRedirectStrategy.INSTANCE;
101+
case FOLLOW -> DefaultRedirectStrategy.INSTANCE;
102+
case DONT_FOLLOW -> NoFollowRedirectStrategy.INSTANCE;
103+
};
104+
}
105+
106+
private PoolingHttpClientConnectionManager createConnectionManager(ClientHttpRequestFactorySettings settings) {
107+
PoolingHttpClientConnectionManagerBuilder builder = PoolingHttpClientConnectionManagerBuilder.create();
108+
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
109+
map.from(settings::readTimeout).as(this::createSocketConfig).to(builder::setDefaultSocketConfig);
110+
map.from(settings::sslBundle).as(this::createTlsSocketStrategy).to(builder::setTlsSocketStrategy);
111+
return builder.useSystemProperties().build();
100112
}
101113

102114
private DefaultClientTlsStrategy createTlsSocketStrategy(SslBundle sslBundle) {
@@ -110,6 +122,30 @@ private SocketConfig createSocketConfig(Duration readTimeout) {
110122
return SocketConfig.custom().setSoTimeout((int) readTimeout.toMillis(), TimeUnit.MILLISECONDS).build();
111123
}
112124

125+
/**
126+
* {@link RedirectStrategy} that never follows redirects.
127+
*/
128+
private static final class NoFollowRedirectStrategy implements RedirectStrategy {
129+
130+
private static final RedirectStrategy INSTANCE = new NoFollowRedirectStrategy();
131+
132+
private NoFollowRedirectStrategy() {
133+
}
134+
135+
@Override
136+
public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context)
137+
throws HttpException {
138+
return false;
139+
}
140+
141+
@Override
142+
public URI getLocationURI(HttpRequest request, HttpResponse response, HttpContext context)
143+
throws HttpException {
144+
return null;
145+
}
146+
147+
}
148+
113149
static class Classes {
114150

115151
static final String HTTP_CLIENTS = "org.apache.hc.client5.http.impl.classic.HttpClients";

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
package org.springframework.boot.http.client;
1818

1919
import java.net.http.HttpClient;
20-
import java.time.Duration;
20+
import java.net.http.HttpClient.Redirect;
2121
import java.util.Collection;
2222
import java.util.List;
2323
import java.util.function.Consumer;
2424

2525
import org.springframework.boot.context.properties.PropertyMapper;
26+
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
2627
import org.springframework.boot.ssl.SslBundle;
2728
import org.springframework.http.client.JdkClientHttpRequestFactory;
2829
import org.springframework.util.ClassUtils;
@@ -59,24 +60,30 @@ public JdkClientHttpRequestFactoryBuilder withCustomizers(
5960

6061
@Override
6162
protected JdkClientHttpRequestFactory createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings) {
62-
HttpClient httpClient = createHttpClient(settings.connectTimeout(), settings.sslBundle());
63+
HttpClient httpClient = createHttpClient(settings);
6364
JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(httpClient);
6465
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
6566
map.from(settings::readTimeout).to(requestFactory::setReadTimeout);
6667
return requestFactory;
6768
}
6869

69-
private HttpClient createHttpClient(Duration connectTimeout, SslBundle sslBundle) {
70+
private HttpClient createHttpClient(ClientHttpRequestFactorySettings settings) {
7071
HttpClient.Builder httpClientBuilder = HttpClient.newBuilder();
71-
if (connectTimeout != null) {
72-
httpClientBuilder.connectTimeout(connectTimeout);
73-
}
74-
if (sslBundle != null) {
75-
httpClientBuilder.sslContext(sslBundle.createSslContext());
76-
}
72+
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
73+
map.from(settings::connectTimeout).to(httpClientBuilder::connectTimeout);
74+
map.from(settings::sslBundle).as(SslBundle::createSslContext).to(httpClientBuilder::sslContext);
75+
map.from(settings::redirects).as(this::asHttpClientRedirect).to(httpClientBuilder::followRedirects);
7776
return httpClientBuilder.build();
7877
}
7978

79+
private Redirect asHttpClientRedirect(Redirects redirects) {
80+
return switch (redirects) {
81+
case FOLLOW_WHEN_POSSIBLE -> Redirect.NORMAL;
82+
case FOLLOW -> Redirect.NORMAL;
83+
case DONT_FOLLOW -> Redirect.NEVER;
84+
};
85+
}
86+
8087
static class Classes {
8188

8289
static final String HTTP_CLIENT = "java.net.http.HttpClient";

0 commit comments

Comments
 (0)