Customizing Per-Request Timeouts for a Singleton RestTemplate with Apache HttpClient

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 RequestConfig created 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 RestTemplate and a single PoolingHttpClientConnectionManager, 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.

Tags: RestTemplate Apache HttpClient RequestConfig java Spring Boot

Posted on Tue, 09 Jun 2026 18:01:25 +0000 by scavok