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..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,8 @@ 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; import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; @@ -53,6 +55,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 +138,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 +559,50 @@ 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; + * 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) { - this.connectionTimeout = connectionTimeout; - return this; - } + public ApiClient setConnectTimeout(int connectionTimeout) { + if (connectionTimeout < 0) { + throw new IllegalArgumentException("connectionTimeout must not be negative"); + } + 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; + * 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; + } /** * Get the date format used to parse/format date parameters. @@ -1150,6 +1189,20 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { HttpClientContext context = HttpClientContext.create(); context.setCookieStore(store); + if (connectionTimeout > 0 || readTimeout > 0) { + // 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)); + } + 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..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,8 @@ 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; import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; @@ -42,6 +44,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 +98,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 +434,50 @@ 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; + * 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) { - this.connectionTimeout = connectionTimeout; - return this; - } + public ApiClient setConnectTimeout(int connectionTimeout) { + if (connectionTimeout < 0) { + throw new IllegalArgumentException("connectionTimeout must not be negative"); + } + 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; + * 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; + } /** * Get the date format used to parse/format date parameters. @@ -1016,6 +1055,20 @@ public T invokeAPI( HttpClientContext context = HttpClientContext.create(); context.setCookieStore(store); + if (connectionTimeout > 0 || readTimeout > 0) { + // 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)); + } + 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..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,8 @@ 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; import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; @@ -44,6 +46,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 +145,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 +529,50 @@ 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; + * 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) { - this.connectionTimeout = connectionTimeout; - return this; - } + public ApiClient setConnectTimeout(int connectionTimeout) { + if (connectionTimeout < 0) { + throw new IllegalArgumentException("connectionTimeout must not be negative"); + } + 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; + * 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; + } /** * Get the date format used to parse/format date parameters. @@ -1115,6 +1154,20 @@ public T invokeAPI( HttpClientContext context = HttpClientContext.create(); context.setCookieStore(store); + if (connectionTimeout > 0 || readTimeout > 0) { + // 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)); + } + 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..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,8 @@ 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; import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; @@ -42,6 +44,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 +143,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 +527,50 @@ 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; + * 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) { - this.connectionTimeout = connectionTimeout; - return this; - } + public ApiClient setConnectTimeout(int connectionTimeout) { + if (connectionTimeout < 0) { + throw new IllegalArgumentException("connectionTimeout must not be negative"); + } + 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; + * 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; + } /** * Get the date format used to parse/format date parameters. @@ -1109,6 +1148,20 @@ public T invokeAPI( HttpClientContext context = HttpClientContext.create(); context.setCookieStore(store); + if (connectionTimeout > 0 || readTimeout > 0) { + // 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)); + } + 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..6319cfd34429 --- /dev/null +++ b/samples/client/petstore/java/apache-httpclient/src/test/java/org/openapitools/client/ApiClientTest.java @@ -0,0 +1,94 @@ +package org.openapitools.client; + +import java.io.IOException; +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; + +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 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 + // 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))); + } + } + + @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. + 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); + PetApi petApi = new PetApi(customApiClient); + assertTimeoutPreemptively(Duration.ofSeconds(30), () -> + assertThrows(ApiException.class, () -> petApi.getPetById(1L))); + } + } +}