Reinstate support for read timeouts with RestTemplateBuilder

Refactor the way `ClientHttpRequestFactory` instances are created in
order to support setting read timeouts.

Prior to this commit, the reflection based approach would call
`setReadTimeout`. As of Spring Framework 6.0, the
`HttpComponentsClientHttpRequestFactory` class no longer supports this
approach. The timeout must be set on the `HttpClientConnectionManager`
used in the `HttpClient` which can be passed in to the constructor.

In order to support this approach, the `ClientHttpRequestFactory` can
now be created using a `Function` rather than a `Supplier`. The
function accepts a `ClientHttpRequestFactorySettings` which provides
the timeout settings to apply.

The `ClientHttpRequestFactories` utility class provides methods to
create `ClientHttpRequestFactory` instances that respect the settings.
Whenever possible, these are created without using reflection.

Fixes gh-32857

Co-authored-by: Phillip Webb <pwebb@vmware.com>
This commit is contained in:
Andy Wilkinson
2022-10-24 17:41:36 +01:00
committed by Phillip Webb
parent c22e76632c
commit e6c37d698f
23 changed files with 1405 additions and 544 deletions

View File

@@ -18,16 +18,22 @@ package org.springframework.boot.test.web.client;
import java.io.IOException;
import java.net.URI;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.cookie.StandardCookieSpec;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
@@ -35,10 +41,12 @@ import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http.ssl.TLS;
import org.apache.hc.core5.ssl.SSLContextBuilder;
import org.springframework.boot.web.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.boot.web.client.RootUriTemplateHandler;
import org.springframework.core.ParameterizedTypeReference;
@@ -138,8 +146,8 @@ public class TestRestTemplate {
if (httpClientOptions != null) {
ClientHttpRequestFactory requestFactory = builder.buildRequestFactory();
if (requestFactory instanceof HttpComponentsClientHttpRequestFactory) {
builder = builder
.requestFactory(() -> new CustomHttpComponentsClientHttpRequestFactory(httpClientOptions));
builder = builder.requestFactory(
(settings) -> new CustomHttpComponentsClientHttpRequestFactory(httpClientOptions, settings));
}
}
if (username != null || password != null) {
@@ -1000,43 +1008,71 @@ public class TestRestTemplate {
private final boolean enableRedirects;
public CustomHttpComponentsClientHttpRequestFactory(HttpClientOption[] httpClientOptions) {
public CustomHttpComponentsClientHttpRequestFactory(HttpClientOption[] httpClientOptions,
ClientHttpRequestFactorySettings settings) {
Set<HttpClientOption> options = new HashSet<>(Arrays.asList(httpClientOptions));
this.cookieSpec = (options.contains(HttpClientOption.ENABLE_COOKIES) ? StandardCookieSpec.STRICT
: StandardCookieSpec.IGNORE);
this.enableRedirects = options.contains(HttpClientOption.ENABLE_REDIRECTS);
if (options.contains(HttpClientOption.SSL)) {
setHttpClient(createSslHttpClient());
boolean ssl = options.contains(HttpClientOption.SSL);
if (settings.readTimeout() != null || ssl) {
setHttpClient(createHttpClient(settings.readTimeout(), ssl));
}
if (settings.connectTimeout() != null) {
setConnectTimeout((int) settings.connectTimeout().toMillis());
}
if (settings.bufferRequestBody() != null) {
setBufferRequestBody(settings.bufferRequestBody());
}
}
private HttpClient createSslHttpClient() {
private HttpClient createHttpClient(Duration readTimeout, boolean ssl) {
try {
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy())
.build();
SSLConnectionSocketFactory socketFactory = SSLConnectionSocketFactoryBuilder.create()
.setSslContext(sslContext).setTlsVersions(TLS.V_1_3, TLS.V_1_2).build();
PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder
.create().setSSLSocketFactory(socketFactory).build();
return HttpClients.custom().setConnectionManager(connectionManager)
.setDefaultRequestConfig(getRequestConfig()).build();
HttpClientBuilder builder = HttpClients.custom();
builder.setConnectionManager(createConnectionManager(readTimeout, ssl));
builder.setDefaultRequestConfig(createRequestConfig());
return builder.build();
}
catch (Exception ex) {
throw new IllegalStateException("Unable to create SSL HttpClient", ex);
throw new IllegalStateException("Unable to create customized HttpClient", ex);
}
}
private PoolingHttpClientConnectionManager createConnectionManager(Duration readTimeout, boolean ssl)
throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException {
PoolingHttpClientConnectionManagerBuilder builder = PoolingHttpClientConnectionManagerBuilder.create();
if (ssl) {
builder.setSSLSocketFactory(createSocketFactory());
}
if (readTimeout != null) {
SocketConfig socketConfig = SocketConfig.custom()
.setSoTimeout((int) readTimeout.toMillis(), TimeUnit.MILLISECONDS).build();
builder.setDefaultSocketConfig(socketConfig);
}
return builder.build();
}
private SSLConnectionSocketFactory createSocketFactory()
throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException {
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy())
.build();
return SSLConnectionSocketFactoryBuilder.create().setSslContext(sslContext)
.setTlsVersions(TLS.V_1_3, TLS.V_1_2).build();
}
@Override
protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
HttpClientContext context = HttpClientContext.create();
context.setRequestConfig(getRequestConfig());
context.setRequestConfig(createRequestConfig());
return context;
}
protected RequestConfig getRequestConfig() {
return RequestConfig.custom().setCookieSpec(this.cookieSpec).setAuthenticationEnabled(false)
.setRedirectsEnabled(this.enableRedirects).build();
protected RequestConfig createRequestConfig() {
RequestConfig.Builder builder = RequestConfig.custom();
builder.setCookieSpec(this.cookieSpec);
builder.setAuthenticationEnabled(false);
builder.setRedirectsEnabled(this.enableRedirects);
return builder.build();
}
}

View File

@@ -134,7 +134,7 @@ class TestRestTemplateTests {
TestRestTemplate template = new TestRestTemplate(HttpClientOption.ENABLE_REDIRECTS);
CustomHttpComponentsClientHttpRequestFactory factory = (CustomHttpComponentsClientHttpRequestFactory) template
.getRestTemplate().getRequestFactory();
RequestConfig config = factory.getRequestConfig();
RequestConfig config = factory.createRequestConfig();
assertThat(config.isRedirectsEnabled()).isTrue();
}