[
https://issues.apache.org/jira/browse/HTTPCLIENT-2405?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18035016#comment-18035016
]
Oleg Kalnichevski commented on HTTPCLIENT-2405:
-----------------------------------------------
[~henrikdohlmann] 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.
{noformat}
[connectTimeout=3 MINUTES, socketTimeout=null, validateAfterInactivity=null,
timeToLive=null]
{noformat}
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
{noformat}
[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]
{noformat}
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.
Oleg
> 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]