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:
+ *
+ * - Any custom {@link SSLContext} or {@link HostnameVerifier} configured in {@link Config}
+ * will be ignored, as the connection manager's own socket factory registry is used.
+ * - Connection-level configurations (such as {@code connectTimeout} and {@code socketTimeout})
+ * must be pre-configured on the shared connection manager, as the timeouts from {@link Config}
+ * will only be applied at the request level (via {@link RequestConfig}).
+ *
+ *
+ *
+ * @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();