From 5a1090dc3ce6c3b436b13bbd0e47d220b17bf4e5 Mon Sep 17 00:00:00 2001 From: treaz Date: Mon, 13 Jul 2026 12:25:11 +0200 Subject: [PATCH 1/3] fix(java/apache-httpclient): apply connect timeout to requests, add read timeout support setConnectTimeout() stored the value in a field that was never read, so the configured timeout was silently ignored and Apache HttpClient's own defaults applied (3-minute connect timeout, unbounded response wait). Wire connectionTimeout into a per-request RequestConfig set on the HttpClientContext in invokeAPI(), and add symmetric getReadTimeout()/ setReadTimeout() support mapped to the response timeout. The per-request approach works with both httpclient5 5.1.x (Gradle template) and 5.2.x (Maven template) and preserves a user-supplied custom CloseableHttpClient instead of rebuilding it. Fixes #24269 --- .../apache-httpclient/ApiClient.mustache | 51 +++++++++++++-- .../org/openapitools/client/ApiClient.java | 51 +++++++++++++-- .../org/openapitools/client/ApiClient.java | 51 +++++++++++++-- .../org/openapitools/client/ApiClient.java | 51 +++++++++++++-- .../openapitools/client/ApiClientTest.java | 62 +++++++++++++++++++ 5 files changed, 242 insertions(+), 24 deletions(-) create mode 100644 samples/client/petstore/java/apache-httpclient/src/test/java/org/openapitools/client/ApiClientTest.java diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/apache-httpclient/ApiClient.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/apache-httpclient/ApiClient.mustache index e6418e5fac26..5534b5f63978 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/apache-httpclient/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/apache-httpclient/ApiClient.mustache @@ -31,6 +31,7 @@ import org.openapitools.jackson.nullable.JsonNullableJackson3Module; {{/useJackson3}} {{/openApiNullable}} +import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.cookie.BasicCookieStore; import org.apache.hc.client5.http.cookie.Cookie; import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; @@ -53,6 +54,7 @@ import org.apache.hc.core5.http.io.entity.FileEntity; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; import org.apache.hc.core5.http.message.BasicNameValuePair; +import org.apache.hc.core5.util.Timeout; import java.util.Collection; import java.util.Collections; @@ -135,6 +137,7 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { protected Map serverVariables = null; protected boolean debugging = false; protected int connectionTimeout = 0; + protected int readTimeout = 0; protected CloseableHttpClient httpClient; protected ObjectMapper objectMapper; @@ -555,15 +558,40 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { /** * Set the connect timeout (in milliseconds). - * A value of 0 means no timeout, otherwise values must be between 1 and - * {@link Integer#MAX_VALUE}. + * A value of 0 means the HTTP client's default connect timeout is used, + * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. + * The timeout is applied to each request via its request configuration + * and takes precedence over the defaults of the underlying HTTP client. * @param connectionTimeout Connection timeout in milliseconds * @return API client */ - public ApiClient setConnectTimeout(int connectionTimeout) { - this.connectionTimeout = connectionTimeout; - return this; - } + public ApiClient setConnectTimeout(int connectionTimeout) { + this.connectionTimeout = connectionTimeout; + return this; + } + + /** + * Read timeout (in milliseconds). + * @return Read timeout + */ + public int getReadTimeout() { + return readTimeout; + } + + /** + * Set the read timeout (in milliseconds), i.e. the response timeout + * while waiting for data after the connection is established. + * A value of 0 means the HTTP client's default response timeout is used, + * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. + * The timeout is applied to each request via its request configuration + * and takes precedence over the defaults of the underlying HTTP client. + * @param readTimeout Read timeout in milliseconds + * @return API client + */ + public ApiClient setReadTimeout(int readTimeout) { + this.readTimeout = readTimeout; + return this; + } /** * Get the date format used to parse/format date parameters. @@ -1150,6 +1178,17 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { HttpClientContext context = HttpClientContext.create(); context.setCookieStore(store); + if (connectionTimeout > 0 || readTimeout > 0) { + RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); + if (connectionTimeout > 0) { + requestConfigBuilder.setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)); + } + if (readTimeout > 0) { + requestConfigBuilder.setResponseTimeout(Timeout.ofMilliseconds(readTimeout)); + } + context.setRequestConfig(requestConfigBuilder.build()); + } + ContentType contentTypeObj = getContentType(contentType); if (body != null || !formParams.isEmpty()) { if (isBodyAllowed(method)) { diff --git a/samples/client/echo_api/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java b/samples/client/echo_api/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java index 2990b6bcd034..20d7e79de759 100644 --- a/samples/client/echo_api/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java +++ b/samples/client/echo_api/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import org.openapitools.jackson.nullable.JsonNullableModule; +import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.cookie.BasicCookieStore; import org.apache.hc.client5.http.cookie.Cookie; import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; @@ -42,6 +43,7 @@ import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; import org.apache.hc.core5.http.message.BasicNameValuePair; +import org.apache.hc.core5.util.Timeout; import java.util.Collection; import java.util.Collections; @@ -95,6 +97,7 @@ public class ApiClient extends JavaTimeFormatter { protected Map serverVariables = null; protected boolean debugging = false; protected int connectionTimeout = 0; + protected int readTimeout = 0; protected CloseableHttpClient httpClient; protected ObjectMapper objectMapper; @@ -430,15 +433,40 @@ public int getConnectTimeout() { /** * Set the connect timeout (in milliseconds). - * A value of 0 means no timeout, otherwise values must be between 1 and - * {@link Integer#MAX_VALUE}. + * A value of 0 means the HTTP client's default connect timeout is used, + * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. + * The timeout is applied to each request via its request configuration + * and takes precedence over the defaults of the underlying HTTP client. * @param connectionTimeout Connection timeout in milliseconds * @return API client */ - public ApiClient setConnectTimeout(int connectionTimeout) { - this.connectionTimeout = connectionTimeout; - return this; - } + public ApiClient setConnectTimeout(int connectionTimeout) { + this.connectionTimeout = connectionTimeout; + return this; + } + + /** + * Read timeout (in milliseconds). + * @return Read timeout + */ + public int getReadTimeout() { + return readTimeout; + } + + /** + * Set the read timeout (in milliseconds), i.e. the response timeout + * while waiting for data after the connection is established. + * A value of 0 means the HTTP client's default response timeout is used, + * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. + * The timeout is applied to each request via its request configuration + * and takes precedence over the defaults of the underlying HTTP client. + * @param readTimeout Read timeout in milliseconds + * @return API client + */ + public ApiClient setReadTimeout(int readTimeout) { + this.readTimeout = readTimeout; + return this; + } /** * Get the date format used to parse/format date parameters. @@ -1016,6 +1044,17 @@ public T invokeAPI( HttpClientContext context = HttpClientContext.create(); context.setCookieStore(store); + if (connectionTimeout > 0 || readTimeout > 0) { + RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); + if (connectionTimeout > 0) { + requestConfigBuilder.setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)); + } + if (readTimeout > 0) { + requestConfigBuilder.setResponseTimeout(Timeout.ofMilliseconds(readTimeout)); + } + context.setRequestConfig(requestConfigBuilder.build()); + } + ContentType contentTypeObj = getContentType(contentType); if (body != null || !formParams.isEmpty()) { if (isBodyAllowed(method)) { diff --git a/samples/client/petstore/java/apache-httpclient-jackson3/src/main/java/org/openapitools/client/ApiClient.java b/samples/client/petstore/java/apache-httpclient-jackson3/src/main/java/org/openapitools/client/ApiClient.java index e64cf2b85dc5..fd205439bf7f 100644 --- a/samples/client/petstore/java/apache-httpclient-jackson3/src/main/java/org/openapitools/client/ApiClient.java +++ b/samples/client/petstore/java/apache-httpclient-jackson3/src/main/java/org/openapitools/client/ApiClient.java @@ -22,6 +22,7 @@ import tools.jackson.core.JacksonException; import org.openapitools.jackson.nullable.JsonNullableJackson3Module; +import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.cookie.BasicCookieStore; import org.apache.hc.client5.http.cookie.Cookie; import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; @@ -44,6 +45,7 @@ import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; import org.apache.hc.core5.http.message.BasicNameValuePair; +import org.apache.hc.core5.util.Timeout; import java.util.Collection; import java.util.Collections; @@ -142,6 +144,7 @@ public class ApiClient extends JavaTimeFormatter { protected Map serverVariables = null; protected boolean debugging = false; protected int connectionTimeout = 0; + protected int readTimeout = 0; protected CloseableHttpClient httpClient; protected ObjectMapper objectMapper; @@ -525,15 +528,40 @@ public int getConnectTimeout() { /** * Set the connect timeout (in milliseconds). - * A value of 0 means no timeout, otherwise values must be between 1 and - * {@link Integer#MAX_VALUE}. + * A value of 0 means the HTTP client's default connect timeout is used, + * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. + * The timeout is applied to each request via its request configuration + * and takes precedence over the defaults of the underlying HTTP client. * @param connectionTimeout Connection timeout in milliseconds * @return API client */ - public ApiClient setConnectTimeout(int connectionTimeout) { - this.connectionTimeout = connectionTimeout; - return this; - } + public ApiClient setConnectTimeout(int connectionTimeout) { + this.connectionTimeout = connectionTimeout; + return this; + } + + /** + * Read timeout (in milliseconds). + * @return Read timeout + */ + public int getReadTimeout() { + return readTimeout; + } + + /** + * Set the read timeout (in milliseconds), i.e. the response timeout + * while waiting for data after the connection is established. + * A value of 0 means the HTTP client's default response timeout is used, + * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. + * The timeout is applied to each request via its request configuration + * and takes precedence over the defaults of the underlying HTTP client. + * @param readTimeout Read timeout in milliseconds + * @return API client + */ + public ApiClient setReadTimeout(int readTimeout) { + this.readTimeout = readTimeout; + return this; + } /** * Get the date format used to parse/format date parameters. @@ -1115,6 +1143,17 @@ public T invokeAPI( HttpClientContext context = HttpClientContext.create(); context.setCookieStore(store); + if (connectionTimeout > 0 || readTimeout > 0) { + RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); + if (connectionTimeout > 0) { + requestConfigBuilder.setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)); + } + if (readTimeout > 0) { + requestConfigBuilder.setResponseTimeout(Timeout.ofMilliseconds(readTimeout)); + } + context.setRequestConfig(requestConfigBuilder.build()); + } + ContentType contentTypeObj = getContentType(contentType); if (body != null || !formParams.isEmpty()) { if (isBodyAllowed(method)) { diff --git a/samples/client/petstore/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java b/samples/client/petstore/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java index 0852b576a426..eace1b74533c 100644 --- a/samples/client/petstore/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java +++ b/samples/client/petstore/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import org.openapitools.jackson.nullable.JsonNullableModule; +import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.cookie.BasicCookieStore; import org.apache.hc.client5.http.cookie.Cookie; import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; @@ -42,6 +43,7 @@ import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; import org.apache.hc.core5.http.message.BasicNameValuePair; +import org.apache.hc.core5.util.Timeout; import java.util.Collection; import java.util.Collections; @@ -140,6 +142,7 @@ public class ApiClient extends JavaTimeFormatter { protected Map serverVariables = null; protected boolean debugging = false; protected int connectionTimeout = 0; + protected int readTimeout = 0; protected CloseableHttpClient httpClient; protected ObjectMapper objectMapper; @@ -523,15 +526,40 @@ public int getConnectTimeout() { /** * Set the connect timeout (in milliseconds). - * A value of 0 means no timeout, otherwise values must be between 1 and - * {@link Integer#MAX_VALUE}. + * A value of 0 means the HTTP client's default connect timeout is used, + * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. + * The timeout is applied to each request via its request configuration + * and takes precedence over the defaults of the underlying HTTP client. * @param connectionTimeout Connection timeout in milliseconds * @return API client */ - public ApiClient setConnectTimeout(int connectionTimeout) { - this.connectionTimeout = connectionTimeout; - return this; - } + public ApiClient setConnectTimeout(int connectionTimeout) { + this.connectionTimeout = connectionTimeout; + return this; + } + + /** + * Read timeout (in milliseconds). + * @return Read timeout + */ + public int getReadTimeout() { + return readTimeout; + } + + /** + * Set the read timeout (in milliseconds), i.e. the response timeout + * while waiting for data after the connection is established. + * A value of 0 means the HTTP client's default response timeout is used, + * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. + * The timeout is applied to each request via its request configuration + * and takes precedence over the defaults of the underlying HTTP client. + * @param readTimeout Read timeout in milliseconds + * @return API client + */ + public ApiClient setReadTimeout(int readTimeout) { + this.readTimeout = readTimeout; + return this; + } /** * Get the date format used to parse/format date parameters. @@ -1109,6 +1137,17 @@ public T invokeAPI( HttpClientContext context = HttpClientContext.create(); context.setCookieStore(store); + if (connectionTimeout > 0 || readTimeout > 0) { + RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); + if (connectionTimeout > 0) { + requestConfigBuilder.setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)); + } + if (readTimeout > 0) { + requestConfigBuilder.setResponseTimeout(Timeout.ofMilliseconds(readTimeout)); + } + context.setRequestConfig(requestConfigBuilder.build()); + } + ContentType contentTypeObj = getContentType(contentType); if (body != null || !formParams.isEmpty()) { if (isBodyAllowed(method)) { diff --git a/samples/client/petstore/java/apache-httpclient/src/test/java/org/openapitools/client/ApiClientTest.java b/samples/client/petstore/java/apache-httpclient/src/test/java/org/openapitools/client/ApiClientTest.java new file mode 100644 index 000000000000..77286bfe667d --- /dev/null +++ b/samples/client/petstore/java/apache-httpclient/src/test/java/org/openapitools/client/ApiClientTest.java @@ -0,0 +1,62 @@ +package org.openapitools.client; + +import java.io.IOException; +import java.net.ServerSocket; +import java.time.Duration; + +import org.junit.jupiter.api.*; +import org.openapitools.client.api.PetApi; + +import static org.junit.jupiter.api.Assertions.*; + +public class ApiClientTest { + ApiClient apiClient = null; + + @BeforeEach + public void setup() { + apiClient = new ApiClient(); + } + + @Test + public void testConnectTimeoutRoundTrip() { + assertEquals(0, apiClient.getConnectTimeout()); + apiClient.setConnectTimeout(15000); + assertEquals(15000, apiClient.getConnectTimeout()); + } + + @Test + public void testReadTimeoutRoundTrip() { + assertEquals(0, apiClient.getReadTimeout()); + apiClient.setReadTimeout(15000); + assertEquals(15000, apiClient.getReadTimeout()); + } + + @Test + public void testConnectTimeoutIsApplied() { + // 192.0.2.1 (RFC 5737 TEST-NET-1) is non-routable: connecting blocks until the + // connect timeout fires. If the configured timeout is not applied to the request, + // Apache HttpClient's own default (3 minutes) is used instead and the assertion + // aborts the test after 30 seconds. + apiClient.setBasePath("http://192.0.2.1:81"); + apiClient.setConnectTimeout(500); + PetApi petApi = new PetApi(apiClient); + assertTimeoutPreemptively(Duration.ofSeconds(30), () -> + assertThrows(ApiException.class, () -> petApi.getPetById(1L))); + } + + @Test + public void testReadTimeoutIsApplied() throws IOException { + // A server socket that is never accepted from still completes the TCP handshake + // (via the OS backlog), so the request is sent and then blocks waiting for the + // response. If the configured timeout is not applied to the request, Apache + // HttpClient waits forever by default and the assertion aborts the test after + // 30 seconds. + try (ServerSocket serverSocket = new ServerSocket(0)) { + apiClient.setBasePath("http://localhost:" + serverSocket.getLocalPort()); + apiClient.setReadTimeout(500); + PetApi petApi = new PetApi(apiClient); + assertTimeoutPreemptively(Duration.ofSeconds(30), () -> + assertThrows(ApiException.class, () -> petApi.getPetById(1L))); + } + } +} From caedbe4356fe09312bca08f3cac62a77ca0fe898 Mon Sep 17 00:00:00 2001 From: treaz Date: Mon, 13 Jul 2026 13:17:17 +0200 Subject: [PATCH 2/3] fix(java/apache-httpclient): preserve custom client request-config defaults, reject negative timeouts Address review feedback: - when building the per-request RequestConfig, start from the default request configuration of the underlying HTTP client (via Configurable) so a caller-supplied client's redirect policy, cookie spec and other request defaults are preserved and only the configured timeouts are overridden - reject negative values in setConnectTimeout()/setReadTimeout() with IllegalArgumentException instead of silently ignoring them --- .../apache-httpclient/ApiClient.mustache | 24 +++++++++++--- .../org/openapitools/client/ApiClient.java | 24 +++++++++++--- .../org/openapitools/client/ApiClient.java | 24 +++++++++++--- .../org/openapitools/client/ApiClient.java | 24 +++++++++++--- .../openapitools/client/ApiClientTest.java | 32 +++++++++++++++++++ 5 files changed, 108 insertions(+), 20 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/apache-httpclient/ApiClient.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/apache-httpclient/ApiClient.mustache index 5534b5f63978..7da3ab56f387 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/apache-httpclient/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/apache-httpclient/ApiClient.mustache @@ -31,6 +31,7 @@ import org.openapitools.jackson.nullable.JsonNullableJackson3Module; {{/useJackson3}} {{/openApiNullable}} +import org.apache.hc.client5.http.config.Configurable; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.cookie.BasicCookieStore; import org.apache.hc.client5.http.cookie.Cookie; @@ -560,12 +561,17 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { * Set the connect timeout (in milliseconds). * A value of 0 means the HTTP client's default connect timeout is used, * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. - * The timeout is applied to each request via its request configuration - * and takes precedence over the defaults of the underlying HTTP client. + * The timeout is applied to each request via its request configuration; + * other request configuration defaults of the underlying HTTP client + * are preserved. * @param connectionTimeout Connection timeout in milliseconds * @return API client + * @throws IllegalArgumentException if connectionTimeout is negative */ public ApiClient setConnectTimeout(int connectionTimeout) { + if (connectionTimeout < 0) { + throw new IllegalArgumentException("connectionTimeout must not be negative"); + } this.connectionTimeout = connectionTimeout; return this; } @@ -583,12 +589,17 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { * while waiting for data after the connection is established. * A value of 0 means the HTTP client's default response timeout is used, * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. - * The timeout is applied to each request via its request configuration - * and takes precedence over the defaults of the underlying HTTP client. + * The timeout is applied to each request via its request configuration; + * other request configuration defaults of the underlying HTTP client + * are preserved. * @param readTimeout Read timeout in milliseconds * @return API client + * @throws IllegalArgumentException if readTimeout is negative */ public ApiClient setReadTimeout(int readTimeout) { + if (readTimeout < 0) { + throw new IllegalArgumentException("readTimeout must not be negative"); + } this.readTimeout = readTimeout; return this; } @@ -1179,7 +1190,10 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { context.setCookieStore(store); if (connectionTimeout > 0 || readTimeout > 0) { - RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); + // start from the default request configuration of the underlying HTTP client, if + // accessible, so that only the configured timeouts are overridden + RequestConfig defaultConfig = httpClient instanceof Configurable ? ((Configurable) httpClient).getConfig() : null; + RequestConfig.Builder requestConfigBuilder = defaultConfig == null ? RequestConfig.custom() : RequestConfig.copy(defaultConfig); if (connectionTimeout > 0) { requestConfigBuilder.setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)); } diff --git a/samples/client/echo_api/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java b/samples/client/echo_api/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java index 20d7e79de759..aeb754b356fb 100644 --- a/samples/client/echo_api/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java +++ b/samples/client/echo_api/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import org.openapitools.jackson.nullable.JsonNullableModule; +import org.apache.hc.client5.http.config.Configurable; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.cookie.BasicCookieStore; import org.apache.hc.client5.http.cookie.Cookie; @@ -435,12 +436,17 @@ public int getConnectTimeout() { * Set the connect timeout (in milliseconds). * A value of 0 means the HTTP client's default connect timeout is used, * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. - * The timeout is applied to each request via its request configuration - * and takes precedence over the defaults of the underlying HTTP client. + * The timeout is applied to each request via its request configuration; + * other request configuration defaults of the underlying HTTP client + * are preserved. * @param connectionTimeout Connection timeout in milliseconds * @return API client + * @throws IllegalArgumentException if connectionTimeout is negative */ public ApiClient setConnectTimeout(int connectionTimeout) { + if (connectionTimeout < 0) { + throw new IllegalArgumentException("connectionTimeout must not be negative"); + } this.connectionTimeout = connectionTimeout; return this; } @@ -458,12 +464,17 @@ public int getReadTimeout() { * while waiting for data after the connection is established. * A value of 0 means the HTTP client's default response timeout is used, * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. - * The timeout is applied to each request via its request configuration - * and takes precedence over the defaults of the underlying HTTP client. + * The timeout is applied to each request via its request configuration; + * other request configuration defaults of the underlying HTTP client + * are preserved. * @param readTimeout Read timeout in milliseconds * @return API client + * @throws IllegalArgumentException if readTimeout is negative */ public ApiClient setReadTimeout(int readTimeout) { + if (readTimeout < 0) { + throw new IllegalArgumentException("readTimeout must not be negative"); + } this.readTimeout = readTimeout; return this; } @@ -1045,7 +1056,10 @@ public T invokeAPI( context.setCookieStore(store); if (connectionTimeout > 0 || readTimeout > 0) { - RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); + // start from the default request configuration of the underlying HTTP client, if + // accessible, so that only the configured timeouts are overridden + RequestConfig defaultConfig = httpClient instanceof Configurable ? ((Configurable) httpClient).getConfig() : null; + RequestConfig.Builder requestConfigBuilder = defaultConfig == null ? RequestConfig.custom() : RequestConfig.copy(defaultConfig); if (connectionTimeout > 0) { requestConfigBuilder.setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)); } diff --git a/samples/client/petstore/java/apache-httpclient-jackson3/src/main/java/org/openapitools/client/ApiClient.java b/samples/client/petstore/java/apache-httpclient-jackson3/src/main/java/org/openapitools/client/ApiClient.java index fd205439bf7f..2537928546e7 100644 --- a/samples/client/petstore/java/apache-httpclient-jackson3/src/main/java/org/openapitools/client/ApiClient.java +++ b/samples/client/petstore/java/apache-httpclient-jackson3/src/main/java/org/openapitools/client/ApiClient.java @@ -22,6 +22,7 @@ import tools.jackson.core.JacksonException; import org.openapitools.jackson.nullable.JsonNullableJackson3Module; +import org.apache.hc.client5.http.config.Configurable; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.cookie.BasicCookieStore; import org.apache.hc.client5.http.cookie.Cookie; @@ -530,12 +531,17 @@ public int getConnectTimeout() { * Set the connect timeout (in milliseconds). * A value of 0 means the HTTP client's default connect timeout is used, * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. - * The timeout is applied to each request via its request configuration - * and takes precedence over the defaults of the underlying HTTP client. + * The timeout is applied to each request via its request configuration; + * other request configuration defaults of the underlying HTTP client + * are preserved. * @param connectionTimeout Connection timeout in milliseconds * @return API client + * @throws IllegalArgumentException if connectionTimeout is negative */ public ApiClient setConnectTimeout(int connectionTimeout) { + if (connectionTimeout < 0) { + throw new IllegalArgumentException("connectionTimeout must not be negative"); + } this.connectionTimeout = connectionTimeout; return this; } @@ -553,12 +559,17 @@ public int getReadTimeout() { * while waiting for data after the connection is established. * A value of 0 means the HTTP client's default response timeout is used, * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. - * The timeout is applied to each request via its request configuration - * and takes precedence over the defaults of the underlying HTTP client. + * The timeout is applied to each request via its request configuration; + * other request configuration defaults of the underlying HTTP client + * are preserved. * @param readTimeout Read timeout in milliseconds * @return API client + * @throws IllegalArgumentException if readTimeout is negative */ public ApiClient setReadTimeout(int readTimeout) { + if (readTimeout < 0) { + throw new IllegalArgumentException("readTimeout must not be negative"); + } this.readTimeout = readTimeout; return this; } @@ -1144,7 +1155,10 @@ public T invokeAPI( context.setCookieStore(store); if (connectionTimeout > 0 || readTimeout > 0) { - RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); + // start from the default request configuration of the underlying HTTP client, if + // accessible, so that only the configured timeouts are overridden + RequestConfig defaultConfig = httpClient instanceof Configurable ? ((Configurable) httpClient).getConfig() : null; + RequestConfig.Builder requestConfigBuilder = defaultConfig == null ? RequestConfig.custom() : RequestConfig.copy(defaultConfig); if (connectionTimeout > 0) { requestConfigBuilder.setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)); } diff --git a/samples/client/petstore/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java b/samples/client/petstore/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java index eace1b74533c..092acc35e1cc 100644 --- a/samples/client/petstore/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java +++ b/samples/client/petstore/java/apache-httpclient/src/main/java/org/openapitools/client/ApiClient.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import org.openapitools.jackson.nullable.JsonNullableModule; +import org.apache.hc.client5.http.config.Configurable; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.cookie.BasicCookieStore; import org.apache.hc.client5.http.cookie.Cookie; @@ -528,12 +529,17 @@ public int getConnectTimeout() { * Set the connect timeout (in milliseconds). * A value of 0 means the HTTP client's default connect timeout is used, * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. - * The timeout is applied to each request via its request configuration - * and takes precedence over the defaults of the underlying HTTP client. + * The timeout is applied to each request via its request configuration; + * other request configuration defaults of the underlying HTTP client + * are preserved. * @param connectionTimeout Connection timeout in milliseconds * @return API client + * @throws IllegalArgumentException if connectionTimeout is negative */ public ApiClient setConnectTimeout(int connectionTimeout) { + if (connectionTimeout < 0) { + throw new IllegalArgumentException("connectionTimeout must not be negative"); + } this.connectionTimeout = connectionTimeout; return this; } @@ -551,12 +557,17 @@ public int getReadTimeout() { * while waiting for data after the connection is established. * A value of 0 means the HTTP client's default response timeout is used, * otherwise values must be between 1 and {@link Integer#MAX_VALUE}. - * The timeout is applied to each request via its request configuration - * and takes precedence over the defaults of the underlying HTTP client. + * The timeout is applied to each request via its request configuration; + * other request configuration defaults of the underlying HTTP client + * are preserved. * @param readTimeout Read timeout in milliseconds * @return API client + * @throws IllegalArgumentException if readTimeout is negative */ public ApiClient setReadTimeout(int readTimeout) { + if (readTimeout < 0) { + throw new IllegalArgumentException("readTimeout must not be negative"); + } this.readTimeout = readTimeout; return this; } @@ -1138,7 +1149,10 @@ public T invokeAPI( context.setCookieStore(store); if (connectionTimeout > 0 || readTimeout > 0) { - RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); + // start from the default request configuration of the underlying HTTP client, if + // accessible, so that only the configured timeouts are overridden + RequestConfig defaultConfig = httpClient instanceof Configurable ? ((Configurable) httpClient).getConfig() : null; + RequestConfig.Builder requestConfigBuilder = defaultConfig == null ? RequestConfig.custom() : RequestConfig.copy(defaultConfig); if (connectionTimeout > 0) { requestConfigBuilder.setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)); } diff --git a/samples/client/petstore/java/apache-httpclient/src/test/java/org/openapitools/client/ApiClientTest.java b/samples/client/petstore/java/apache-httpclient/src/test/java/org/openapitools/client/ApiClientTest.java index 77286bfe667d..baf038511062 100644 --- a/samples/client/petstore/java/apache-httpclient/src/test/java/org/openapitools/client/ApiClientTest.java +++ b/samples/client/petstore/java/apache-httpclient/src/test/java/org/openapitools/client/ApiClientTest.java @@ -4,6 +4,10 @@ import java.net.ServerSocket; import java.time.Duration; +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.core5.util.Timeout; import org.junit.jupiter.api.*; import org.openapitools.client.api.PetApi; @@ -31,6 +35,12 @@ public void testReadTimeoutRoundTrip() { assertEquals(15000, apiClient.getReadTimeout()); } + @Test + public void testNegativeTimeoutsAreRejected() { + assertThrows(IllegalArgumentException.class, () -> apiClient.setConnectTimeout(-1)); + assertThrows(IllegalArgumentException.class, () -> apiClient.setReadTimeout(-1)); + } + @Test public void testConnectTimeoutIsApplied() { // 192.0.2.1 (RFC 5737 TEST-NET-1) is non-routable: connecting blocks until the @@ -59,4 +69,26 @@ public void testReadTimeoutIsApplied() throws IOException { assertThrows(ApiException.class, () -> petApi.getPetById(1L))); } } + + @Test + public void testCustomClientRequestConfigDefaultsArePreserved() throws IOException { + // A custom client is supplied with a default response timeout, while only the + // connect timeout is set on the ApiClient. The per-request configuration must + // start from the custom client's defaults, so the request against a socket that + // never responds must still fail with the client's 500 ms response timeout + // instead of waiting forever. + CloseableHttpClient customClient = HttpClients.custom() + .setDefaultRequestConfig(RequestConfig.custom() + .setResponseTimeout(Timeout.ofMilliseconds(500)) + .build()) + .build(); + try (ServerSocket serverSocket = new ServerSocket(0)) { + ApiClient customApiClient = new ApiClient(customClient); + customApiClient.setBasePath("http://localhost:" + serverSocket.getLocalPort()); + customApiClient.setConnectTimeout(5000); + PetApi petApi = new PetApi(customApiClient); + assertTimeoutPreemptively(Duration.ofSeconds(30), () -> + assertThrows(ApiException.class, () -> petApi.getPetById(1L))); + } + } } From b3fa16117828e84e1162044bd61fd6aeb24de74c Mon Sep 17 00:00:00 2001 From: treaz Date: Mon, 13 Jul 2026 14:29:27 +0200 Subject: [PATCH 3/3] test(java/apache-httpclient): close custom HTTP client in test via try-with-resources --- .../java/org/openapitools/client/ApiClientTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/client/petstore/java/apache-httpclient/src/test/java/org/openapitools/client/ApiClientTest.java b/samples/client/petstore/java/apache-httpclient/src/test/java/org/openapitools/client/ApiClientTest.java index baf038511062..6319cfd34429 100644 --- a/samples/client/petstore/java/apache-httpclient/src/test/java/org/openapitools/client/ApiClientTest.java +++ b/samples/client/petstore/java/apache-httpclient/src/test/java/org/openapitools/client/ApiClientTest.java @@ -77,12 +77,12 @@ public void testCustomClientRequestConfigDefaultsArePreserved() throws IOExcepti // start from the custom client's defaults, so the request against a socket that // never responds must still fail with the client's 500 ms response timeout // instead of waiting forever. - CloseableHttpClient customClient = HttpClients.custom() - .setDefaultRequestConfig(RequestConfig.custom() - .setResponseTimeout(Timeout.ofMilliseconds(500)) - .build()) - .build(); - try (ServerSocket serverSocket = new ServerSocket(0)) { + try (CloseableHttpClient customClient = HttpClients.custom() + .setDefaultRequestConfig(RequestConfig.custom() + .setResponseTimeout(Timeout.ofMilliseconds(500)) + .build()) + .build(); + ServerSocket serverSocket = new ServerSocket(0)) { ApiClient customApiClient = new ApiClient(customClient); customApiClient.setBasePath("http://localhost:" + serverSocket.getLocalPort()); customApiClient.setConnectTimeout(5000);