[ 
https://issues.apache.org/jira/browse/HTTPCLIENT-2405?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Oleg Kalnichevski resolved HTTPCLIENT-2405.
-------------------------------------------
    Resolution: Invalid

> ConnectionReuse uses connectTimeout as readTimeout on reused connection
> -----------------------------------------------------------------------
>
>                 Key: HTTPCLIENT-2405
>                 URL: https://issues.apache.org/jira/browse/HTTPCLIENT-2405
>             Project: HttpComponents HttpClient
>          Issue Type: Bug
>          Components: HttpClient (classic)
>    Affects Versions: 5.5.1
>            Reporter: Henrik Dohlmann
>            Priority: Major
>         Attachments: timeout.zip
>
>
> I was asked to post here from 
> [https://github.com/spring-projects/spring-boot/issues/47895]
> It looks like HTTPCLIENT-2386 introduced a weird bug.
> 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:
> {code:java}
> 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() } {code}
> application.properties:
> {code:java}
> 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 {code}
> TimeoutApplication.java:
> {code:java}
> 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";
>     }
> } {code}
> TimeoutApplicationTest.java:
> {code:java}
> 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();
>     }
> } {code}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to