Skip to content

Deprecate setConnectTimeout on HttpComponentsClientHttpRequestFactory #35748

Closed

Description

@HenrikPublic

Spring Boot 3.5.7 upgrades Apache HttpClient5 from 5.5 to 5.5.1.

We now see a strange error when using RestTemplates with SSL and with the default ConnectionReuseStrategy.
On the second call (that re-uses the connection), the call fails with a SocketTimeOutException (Read timed out) matching the connectTimeout value. So, somehow the reused connection suddenly use connectTimeout for readTimeout.

If we do not use SSL, it works as expected.
If we configure the RestTemplate without connection reuse, it works as expected.
If we downgrade to httpclient5 version 5.5, it works as expected.

Here is a minimal test that recreates the problem:

build.gradle:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.5.7'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.test'
version = '0.0.1-SNAPSHOT'
description = 'timeout'

java { toolchain { languageVersion = JavaLanguageVersion.of(24) } }

repositories { mavenCentral() }

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-logging'
    implementation "org.apache.httpcomponents.client5:httpclient5"
    implementation 'com.google.guava:guava:33.5.0-jre'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') { useJUnitPlatform() }

TimeOutApplication:

package com.test.timeout;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;

@RestController
@RequestMapping("/timeout")
@SpringBootApplication
public class TimeoutApplication {
    public static final Duration CONTROLLER_SLEEP_TIME = Duration.ofSeconds(2);

    public static void main(String[] args) {
        SpringApplication.run(TimeoutApplication.class, args);
    }

    @GetMapping()
    public String timeout() throws Exception {
        Thread.sleep(CONTROLLER_SLEEP_TIME);
        return "OK";
    }

}

application.properties:

spring.application.name=timeout

# NOTE: Generate the keystore and add it to src/main/resource
# keytool -genkeypair -keystore keystore.p12 -storetype PKCS12 -storepass password -alias test -keyalg RSA -keysize 2048 -validity 365 -dname "CN=localhost, OU=tst, O=tst, L=tst, ST=tst, C=tst"
spring.ssl.bundle.jks.mybundle.keystore.location=classpath:keystore.p12
spring.ssl.bundle.jks.mybundle.keystore.password=password
spring.ssl.bundle.jks.mybundle.keystore.type=PKCS12
spring.ssl.bundle.jks.mybundle.key.alias=test
server.ssl.bundle=mybundle

spring.ssl.bundle.jks.clientbundle.truststore.location=classpath:keystore.p12
spring.ssl.bundle.jks.clientbundle.truststore.password=password

server.ssl.enabled=true

logging.level.org.apache.hc.client5.http=DEBUG

TimeoutApplicationTest:

package com.test.timeout;

import com.google.common.base.Stopwatch;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
import static org.assertj.core.api.Assertions.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TimeoutApplicationTests {
    private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(1);
    private static final Duration READ_TIMEOUT = Duration.ofSeconds(10);
    private static final Duration ALLOWED_DIFFERENCE = Duration.ofMillis(200);

    @Autowired
    private RestTemplateBuilder restTemplateBuilder;
    @Autowired
    private SslBundles sslBundles;
    @LocalServerPort
    private int port;

    record Result(Duration runtime, boolean timeout) { }

    @Test
    void with_connection_reuse_throws_socket_timeout_exception_after_connect_timeout_at_second_call() {
        boolean connectionReuse = true;
        var restTemplate = getRestTemplate(connectionReuse);

        Result call1 = callEndpoint(restTemplate);
        Result call2 = callEndpoint(restTemplate);

        assertThat(call1.runtime()).isCloseTo(TimeoutApplication.CONTROLLER_SLEEP_TIME, ALLOWED_DIFFERENCE);
        assertThat(call1.timeout()).isFalse();
        assertThat(call2.runtime()).isCloseTo(CONNECT_TIMEOUT, ALLOWED_DIFFERENCE);
        assertThat(call2.timeout()).isTrue();
    }

    @Test
    void without_connection_reuse() {
        boolean connectionReuse = false;
        var restTemplate = getRestTemplate(connectionReuse);

        Result call1 = callEndpoint(restTemplate);
        Result call2 = callEndpoint(restTemplate);

        assertThat(call1.runtime()).isCloseTo(TimeoutApplication.CONTROLLER_SLEEP_TIME, ALLOWED_DIFFERENCE);
        assertThat(call1.timeout()).isFalse();
        assertThat(call2.runtime()).isCloseTo(TimeoutApplication.CONTROLLER_SLEEP_TIME, ALLOWED_DIFFERENCE);
        assertThat(call2.timeout()).isFalse();
    }

    private Result callEndpoint(RestTemplate restTemplate) {
        String url = "https://localhost:" + port + "/timeout";

        Stopwatch stopwatch = Stopwatch.createStarted();
        try {
            restTemplate.getForObject(url, String.class);
            return new Result(stopwatch.elapsed(), false);
        } catch (Exception e) {
            return new Result(stopwatch.elapsed(), true);
        }
    }

    private RestTemplate getRestTemplate(boolean connectionReuse) {
        var factorySettings = ClientHttpRequestFactorySettings
                .ofSslBundle(sslBundles.getBundle("clientbundle"))
                .withConnectTimeout(CONNECT_TIMEOUT)
                .withReadTimeout(READ_TIMEOUT);
        var factoryBuilder = ClientHttpRequestFactoryBuilder.httpComponents();
        if (!connectionReuse) {
            factoryBuilder = factoryBuilder
                    .withHttpClientCustomizer(b -> b.setConnectionReuseStrategy((_, _, _) -> false));
        }

        return restTemplateBuilder
                .requestFactoryBuilder(factoryBuilder)
                .requestFactorySettings(factorySettings)
                .build();
    }

}

Activity

  1. self-assigned this
    on Oct 31, 2025
  2. wilkinsona commented on Oct 31, 2025

    @wilkinsona
    Member

    Thanks for the sample.

    If we downgrade to httpclient5 version 5.5, it works as expected.

    Given that this is the case, you appear to have identified that the Apache HTTP Client is the cause.

    So, somehow the reused connection suddenly use connectTimeout for readTimeout.

    I think that's due to apache/httpcomponents-client@8094bac. The Apache HTTP Client now uses the connect timeout as a default timeout for the TLS handshake but it appears that it doesn't then re-apply the read timeout once the handshake has been completed. I've also observed that the problem only occurs when TLS is involved, another pointer towards apache/httpcomponents-client@8094bac being the cause. I don't fully understand why this only happens when the connection is being re-used, but I think it'll have to be addressed in the Apache HTTP Client.

  3. added
    status: invalidAn issue that we don't feel is valid
    and removed on Oct 31, 2025
  4. HenrikPublic commented on Oct 31, 2025

    @HenrikPublic
    Author

    I'll try to open a bug with them, then.

  5. HenrikPublic commented on Oct 31, 2025

    @HenrikPublic
    Author

    Is there any way to set the TLS timeout on a RestTemplate?

  6. bclozel commented on Oct 31, 2025

    @bclozel
    Member

    @HenrikPublic no, this is specific to HTTP libraries.

  7. HenrikPublic commented on Oct 31, 2025

    @HenrikPublic
    Author
  8. HenrikPublic commented on Nov 3, 2025

    @HenrikPublic
    Author

    I got a response from Oleg Kainichevski:

    You are not going to like it but this is how it is. As I suspected the read timeout does not get correctly applied to the connection settings that causes the connection manager to lease the connection with the socket timeout as is, that is with the setting from the previous message exchange.

    [connectTimeout=3 MINUTES, socketTimeout=null, validateAfterInactivity=null, timeToLive=null]

    Spring still uses deprecated methods in RequestConfig to set the connection level timeouts and instead of using ConnectionConfig

    They could still abuse the response timeout for HTTP/1.1 requests but it is also set to null

    [expectContinueEnabled=false, proxy=null, cookieSpec=null, redirectsEnabled=true, maxRedirects=50, circularRedirectsAllowed=false, authenticationEnabled=true, targetPreferredAuthSchemes=null, proxyPreferredAuthSchemes=null, connectionRequestTimeout=3 MINUTES, connectTimeout=1000 MILLISECONDS, responseTimeout=null, connectionKeepAlive=3 MINUTES, contentCompressionEnabled=true, hardCancellationEnabled=true, protocolUpgradeEnabled=true]

    I am afraid you will have to take it back to the Spring project. If they need our (my) help figuring out how to replace the deprecated APIs please do let me know.

    I have asked him to please picth in here, if there are further questions, so i don't need to be the poor man-in-the-middle :-)

  9. wilkinsona commented on Nov 3, 2025

    @wilkinsona
    Member

    Thanks, @HenrikPublic. In that case, I'll re-open this issue so that we can transfer it to Spring Framework. It's code in HttpComponentsClientHttpRequestFactory that is calling the deprecated setConnectTimeout method.

  10. removed their assignment
    on Nov 3, 2025
  11. 11 remaining items

  12. changed the title [-]Using deprecated setConnectTimeout method results in incorrect read timeout upon connection reuse when using SSL[/-] [+]Deprecate setConnectTimeout on HttpComponentsClientHttpRequestFactory[/+] on Nov 3, 2025
  13. bclozel commented on Nov 3, 2025

    @bclozel
    Member

    I think we'll need to deprecate HttpComponentsClientHttpRequestFactory#setConnectTimeout and remove it entirely in Spring Framework 7.0. The behavior is currently broken as demonstrated by this issue, and there is no other way to set that configuration once the HttpClient instance is built. HttpComponentsClientHttpRequestFactory has no control over this, since the immutable client instance is given to the constructor or set by the caller.

  14. added a commit that references this issue on Nov 3, 2025
    d65de19
  15. github-actions commented on Nov 3, 2025

    @github-actions
    Contributor

    Fixed via d65de19

  16. dagerber commented on Nov 4, 2025

    @dagerber

    You just deprecated the usage, but the issue is not fixed.
    Why did you close the issue?

  17. bclozel commented on Nov 4, 2025

    @bclozel
    Member

    @dagerber I have deprecated the method configuring this and removed it in Spring Framework 7.0. Do you have an alternative fix in mind?

  18. dagerber commented on Nov 4, 2025

    @dagerber

    @bclozel I have not looked into the Spring Code. From a user perspective, we are setting the readTimeout by providing a ClientHttpRequestFactorySettings Bean like so and we get exactly the problem described in this issue.
    @Bean public ClientHttpRequestFactorySettings restClientFactorySettings() { return ClientHttpRequestFactorySettings.defaults() .withConnectTimeout(Duration.ofMillis(props.getConnectTimeout())) .withReadTimeout(Duration.ofMillis(props.getReadTimeout())); }
    Just be deprecating/removing methods, I guess the problem still exists for us?

  19. bclozel commented on Nov 4, 2025

    @bclozel
    Member

    This issue is not related to readTimeout at all, so this part should be fine because in the HttpClient this is dealt at the request configuration level. I don't think this withConnectTimeout option will work in the future in Spring Boot, because Spring Boot will not be able to configure this value anymore. This value needs to be configured in the HttpClient builder; once built, this value is immutable as far as I understand. We cannot support this way of setting this value anymore.

    We kept it so far even with the deprecated flag, but as this issue shows, this is not working as it should so we need to retire this option and let developers configure this on HttpClient directly.

  20. dagerber commented on Nov 4, 2025

    @dagerber

    This issue is related to readTimeout, if you read the description of @HenrikPublic :

    ... .So, somehow the reused connection suddenly use connectTimeout for readTimeout....
    This is exactly our problem.

    If the configuration of the timeouts only works with setting the configuration on the HttpClient direclty, you should consider cleaning up APIs like the one we used in ClientHttpRequestFactorySettings (and maybe others) and re-open this issue.

  21. bclozel commented on Nov 4, 2025

    @bclozel
    Member

    If you read the analysis from the HttpClient team this explains the problem:

    #35748 (comment)

    If the configuration of the timeouts only works with setting the configuration on the HttpClient direclty, you should consider cleaning up APIs like the one we used in ClientHttpRequestFactorySettings (and maybe others) and re-open this issue.

    ClientHttpRequestFactorySettings belongs to Spring Boot, not Spring Framework, so we'll tackle this problem there.

  22. HenrikPublic commented on Nov 4, 2025

    @HenrikPublic
    Author

    Will there be a related Spring Boot issue targeted for 3.5.8, then?

  23. bclozel commented on Nov 4, 2025

    @bclozel
    Member

    I have created spring-projects/spring-boot#47940 - I'm not sure we can support this particular option anymore on the Spring Boot side for the HttpComponents HttpClient.

  24. jpsla94 commented on Apr 17, 2026

    @jpsla94

    Hello,

    Is there a way to still use the option to set the connection timeout with RestTemplate? Or the only option is to use ConnectionConfig.setConnectTimeout() from PoolingHttpClientConnectionManager?

    Best regards

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

in: webIssues in web modules (web, webmvc, webflux, websocket)type: bugA general bug

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions