diff --git a/src/main/java/com/adyen/httpclient/AdyenHttpClient.java b/src/main/java/com/adyen/httpclient/AdyenHttpClient.java index 192953a52..ba5ba35e1 100644 --- a/src/main/java/com/adyen/httpclient/AdyenHttpClient.java +++ b/src/main/java/com/adyen/httpclient/AdyenHttpClient.java @@ -60,7 +60,9 @@ import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; import org.apache.hc.core5.http.ContentType; @@ -97,9 +99,33 @@ public class AdyenHttpClient implements ClientInterface { private static final String CHARSET = "UTF-8"; private Proxy proxy; + private final PoolingHttpClientConnectionManager sharedConnectionManager; private volatile CloseableHttpClient sharedHttpClient; private final Object lock = new Object(); + public AdyenHttpClient() { + this(null); + } + + /** + * Creates an AdyenHttpClient with a shared connection manager. + *

+ * Note: When using a shared connection manager: + *

+ *

+ * + * @param connectionManager the shared connection manager to use + */ + public AdyenHttpClient(PoolingHttpClientConnectionManager connectionManager) { + this.sharedConnectionManager = connectionManager; + } + /** * Returns the proxy configured for this HTTP client. * @@ -500,7 +526,12 @@ private CloseableHttpClient createHttpClientWithSocketFactory( config.getConnectionRequestTimeoutMillis(), TimeUnit.MILLISECONDS) .setDefaultKeepAlive(config.getDefaultKeepAliveMillis(), TimeUnit.MILLISECONDS) .build(); - ConnectionConfig connectionConfig = + + HttpClientBuilder clientBuilder = HttpClients.custom(); + if (sharedConnectionManager != null) { + clientBuilder = clientBuilder.setConnectionManager(sharedConnectionManager).setConnectionManagerShared(true); + } else { + ConnectionConfig connectionConfig = ConnectionConfig.custom() .setConnectTimeout(config.getConnectionTimeoutMillis(), TimeUnit.MILLISECONDS) // socketTimeout acts as an OS-level safety net for stalled reads; @@ -509,12 +540,14 @@ private CloseableHttpClient createHttpClientWithSocketFactory( // fires first. .setSocketTimeout(config.getReadTimeoutMillis(), TimeUnit.MILLISECONDS) .build(); - return HttpClients.custom() - .setConnectionManager( + clientBuilder = clientBuilder.setConnectionManager( PoolingHttpClientConnectionManagerBuilder.create() .setSSLSocketFactory(socketFactory) .setDefaultConnectionConfig(connectionConfig) - .build()) + .build()); + } + + return clientBuilder .setDefaultRequestConfig(defaultRequestConfig) .setRedirectStrategy(new AdyenCustomRedirectStrategy()) .build(); diff --git a/src/test/java/com/adyen/httpclient/ClientTest.java b/src/test/java/com/adyen/httpclient/ClientTest.java index afeb7a858..f65735a52 100644 --- a/src/test/java/com/adyen/httpclient/ClientTest.java +++ b/src/test/java/com/adyen/httpclient/ClientTest.java @@ -1,6 +1,9 @@ package com.adyen.httpclient; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import com.adyen.BaseTest; import com.adyen.Client; @@ -10,6 +13,8 @@ import com.adyen.enums.Region; import com.adyen.model.RequestOptions; import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -22,6 +27,7 @@ import org.apache.hc.client5.http.config.Configurable; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; import org.junit.jupiter.api.Test; @@ -29,6 +35,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.platform.commons.util.ReflectionUtils; import org.mockito.Mock; public class ClientTest extends BaseTest { @@ -184,6 +191,76 @@ public void testDefaultRequestConfigTimeoutsWithConfigDefaults() throws Exceptio } } + @Test + public void testInjectedConnectionManagerAppliesDefaultRequestConfig() throws Exception { + PoolingHttpClientConnectionManager connectionManager = + mock(PoolingHttpClientConnectionManager.class); + Config config = new Config(); + config.setReadTimeoutMillis(15000); + config.setConnectionRequestTimeoutMillis(30000); + config.setDefaultKeepAliveMillis(40000); + + try (AdyenHttpClient adyenHttpClient = new AdyenHttpClient(connectionManager); + CloseableHttpClient httpClient = adyenHttpClient.createCloseableHttpClient(config)) { + assertInstanceOf(Configurable.class, httpClient); + RequestConfig defaultConfig = ((Configurable) httpClient).getConfig(); + assertNotNull(defaultConfig); + assertEquals(15000, defaultConfig.getResponseTimeout().toMilliseconds()); + assertEquals(30000, defaultConfig.getConnectionRequestTimeout().toMilliseconds()); + } + } + + @Test + public void testInjectedConnectionManagerPreservesPoolLimits() throws Exception { + PoolingHttpClientConnectionManager connectionManager = + new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(50); + connectionManager.setDefaultMaxPerRoute(20); + + try (AdyenHttpClient adyenHttpClient = new AdyenHttpClient(connectionManager); + CloseableHttpClient httpClient = + adyenHttpClient.createCloseableHttpClient(new Config())) { + assertNotNull(httpClient); + assertEquals(50, connectionManager.getMaxTotal()); + assertEquals(20, connectionManager.getDefaultMaxPerRoute()); + } finally { + connectionManager.close(); + } + } + + @Test + public void testClosingHttpClientDoesNotCloseInjectedConnectionManager() throws Exception { + PoolingHttpClientConnectionManager connectionManager = + mock(PoolingHttpClientConnectionManager.class); + + try (AdyenHttpClient adyenHttpClient = new AdyenHttpClient(connectionManager); + CloseableHttpClient httpClient = adyenHttpClient.createCloseableHttpClient(new Config())) { + assertNotNull(httpClient); + adyenHttpClient.close(); + } + + verify(connectionManager, never()).close(); + } + + @Test + public void testClosingAdyenHttpClientClosesDefaultConnectionManager() throws Exception { + PoolingHttpClientConnectionManager connManager; + try (AdyenHttpClient adyenHttpClient = new AdyenHttpClient()) { + + // invoke getOrCreateHttpClient + Method method = AdyenHttpClient.class.getDeclaredMethod("getOrCreateHttpClient", Config.class); + CloseableHttpClient httpClient = (CloseableHttpClient) ReflectionUtils.invokeMethod(method, adyenHttpClient, new Config()); + + // extract connection + Field connManagerField = httpClient.getClass().getDeclaredField("connManager"); + + connManager = (PoolingHttpClientConnectionManager) ReflectionUtils.tryToReadFieldValue(connManagerField, httpClient).get(); + } + + assertNotNull(connManager); + assertEquals(true, connManager.isClosed()); + } + @Test public void testPerRequestConfigIncludesAllTimeouts() throws Exception { AdyenHttpClient adyenHttpClient = new AdyenHttpClient();