Business Context
In a typical Spring application, you might configure a single, shared instance of RestTemplate backed by Apache HttpClient. While a global timeout setting works for most endpoints, specific operations—such as downloading large files or interacting with slow third-party APIs—often require extended timeout durations. The challenge is to apply these specific settings without instantiating multiple RestTemplate objects, which would lead to isolated connection pools and inefficient resource usage.
Core Strategy
RestTemplate allows for a hierarchy of configurations: Request-level settings override Global settings, which in turn override Library Defaults. By leveraging Apache HttpClient's RequestConfig at the individual request level, we can customize timeouts specifically for the calls that need them, while keeping the shared connection pool intact.
Pitfalls to Avoid
| Anti-Pattern | Consequence |
|---|---|
| Multiple RestTemplate Beans | Leads to separate connection pools, increasing memory overhead and limiting total connection availability. |
| Modifying Global Config at Runtime | Creates race conditions; affects concurrent requests thatt should be using the default timeout. |
| Complex Intercepter Logic | Interceptors manipulating timeouts can introduce thread-safety issues and brittle code. |
Technical Implementation
1. Base Configuration (Singleton Setup)
First, we define the shared infrastructure: a connection pool manager, a default request configuration, and the RestTemplate bean.
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
@Configuration
public class HttpClientSetup {
@Bean
public PoolingHttpClientConnectionManager poolManager() {
PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager();
manager.setMaxConnTotal(200);
manager.setDefaultMaxPerRoute(50);
return manager;
}
@Bean
public RequestConfig defaultRequestSettings() {
return RequestConfig.custom()
.setConnectTimeout(5000) // 5 seconds
.setResponseTimeout(10000) // 10 seconds
.setConnectionRequestTimeout(3000) // 3 seconds
.build();
}
@Bean
public CloseableHttpClient apacheHttpClient(PoolingHttpClientConnectionManager poolManager,
RequestConfig defaultRequestSettings) {
return HttpClients.custom()
.setConnectionManager(poolManager)
.setDefaultRequestConfig(defaultRequestSettings)
.build();
}
@Bean
public RestTemplate singletonRestTemplate(CloseableHttpClient apacheHttpClient) {
return new RestTemplate(new HttpComponentsClientHttpRequestFactory(apacheHttpClient));
}
}
2. Implementing Request-Specific Timeouts
To override the default for a specific call, we use RestTemplate.execute along with a RequestCallback. This allows us to inject a custom RequestConfig into the outgoing HTTP request.
import org.apache.hc.client5.http.config.RequestConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.RestTemplate;
@Service
public class ApiService {
@Autowired
private RestTemplate singletonRestTemplate;
public String fetchWithStandardTimeout(String path) {
return singletonRestTemplate.getForObject(path, String.class);
}
public String fetchWithExtendedTimeout(String path) {
// 1. Copy defaults and customize
RequestConfig specificConfig = RequestConfig.copy(
singletonRestTemplate.getRequestFactory()
.getHttpClient()
.getDefaultRequestConfig()
).setConnectTimeout(10000) // 10 seconds
.setResponseTimeout(30000) // 30 seconds
.build();
// 2. Attach custom config via callback
RequestCallback customCallback = request -> {
((HttpComponentsClientHttpRequest) request).getHttpUriRequest()
.setConfig(specificConfig);
};
// 3. Execute
return singletonRestTemplate.execute(
path,
HttpMethod.GET,
customCallback,
response -> {
if (response.getStatusCode().is2xxSuccessful()) {
return new String(response.getBody().readAllBytes());
}
throw new RuntimeException("Failed: " + response.getStatusCode());
}
);
}
}
3. Utility Class for Cleaner Code
To avoid repeating the boilerplate logic, we can extract the timeout customization into a reusable utility.
import org.apache.hc.client5.http.config.RequestConfig;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.RestTemplate;
import java.util.function.Function;
public class TimeoutAdjuster {
public static <T> T runWithCustomTimeout(
RestTemplate template,
String url,
HttpMethod method,
int connectMs,
int readMs,
Function<org.springframework.http.client.ClientHttpResponse, T> handler) {
RequestConfig base = template.getRequestFactory()
.getHttpClient()
.getDefaultRequestConfig();
RequestConfig tunedConfig = RequestConfig.copy(base)
.setConnectTimeout(connectMs)
.setResponseTimeout(readMs)
.build();
RequestCallback action = req -> ((HttpComponentsClientHttpRequest) req)
.getHttpUriRequest().setConfig(tunedConfig);
return template.execute(url, method, action, handler);
}
}
4. Usage Example
public String downloadLargeFile(String url) {
return TimeoutAdjuster.runWithCustomTimeout(
singletonRestTemplate,
url,
HttpMethod.GET,
15000, // 15s connect
60000, // 60s read
response -> new String(response.getBody().readAllBytes())
);
}
Key Considerations
- Thread Safety: The
RequestConfigcreated inside the method is a local variable and specific to that request execution. It does not affect the global singleton settings or other threads. - Connection Pooling: Because we are using a single
RestTemplateand a singlePoolingHttpClientConnectionManager, all requests—regardless of their timeout settings—share the same pool of connections. - Exception Handling: Ensure that code calling the extended-timeout methods handles specific timeout exceptions (like
SocketTimeoutException) appropriately, separate from your standard API error handling.
| Concept | Detail |
|---|---|
| Configuration Scope | Request-level overrides Global defaults. |
| Execution Method | Use RestTemplate.execute() with RequestCallback. |
| Resource Efficiency | Single pool manager ensures optimal connection reuse. |