You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ab...@apache.org on 2026/10/07 10:44:43 UTC

(httpcomponents-client) branch master updated: HTTPCLIENT-2429: Improve support for custom transport layers Expose supported custom transport extension points for classic and async clients.

This is an automated email from the ASF dual-hosted git repository.

arturobernalg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git


The following commit(s) were added to refs/heads/master by this push:
     new 4e5986977 HTTPCLIENT-2429: Improve support for custom transport layers Expose supported custom transport extension points for classic and async clients.
4e5986977 is described below

commit 4e5986977996c6e285f5e0ae2803850bbbb2d133
Author: Arturo Bernal <ab...@apache.org>
AuthorDate: Sat Aug 22 14:19:15 2026 +0200

    HTTPCLIENT-2429: Improve support for custom transport layers
    Expose supported custom transport extension points for classic and async clients.
---
 .../http/impl/async/H2AsyncClientBuilder.java      | 20 ++++-
 .../http/impl/async/HttpAsyncClientBuilder.java    | 20 ++++-
 .../PoolingHttpClientConnectionManagerBuilder.java | 20 ++++-
 .../impl/async/CustomTransportBuilderTest.java     | 72 ++++++++++++++++++
 .../async/SocketChannelFactoryBuilderTest.java     | 88 ++++++++++++++++++++++
 .../impl/io/DetachedSocketFactoryBuilderTest.java  | 64 ++++++++++++++++
 6 files changed, 281 insertions(+), 3 deletions(-)

diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java
index ed52ed54d..7ccb31ea6 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java
@@ -29,6 +29,8 @@
 
 import java.io.Closeable;
 import java.io.IOException;
+import java.net.SocketAddress;
+import java.nio.channels.SocketChannel;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedList;
@@ -97,6 +99,7 @@
 import org.apache.hc.core5.http2.protocol.H2RequestContent;
 import org.apache.hc.core5.http2.protocol.H2RequestTargetHost;
 import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.io.IOFunction;
 import org.apache.hc.core5.reactor.Command;
 import org.apache.hc.core5.reactor.DefaultConnectingIOReactor;
 import org.apache.hc.core5.reactor.IOEventHandlerFactory;
@@ -172,6 +175,7 @@ private ExecInterceptorEntry(
     }
 
     private IOReactorConfig ioReactorConfig;
+    private IOFunction<SocketAddress, SocketChannel> socketChannelFactory;
     private IOSessionListener ioSessionListener;
     private H2Config h2Config;
     private CharCodingConfig charCodingConfig;
@@ -247,6 +251,19 @@ public final H2AsyncClientBuilder setIOReactorConfig(final IOReactorConfig ioRea
         return this;
     }
 
+    /**
+     * Sets the factory used by the I/O reactor to create socket channels for
+     * outgoing connections.
+     *
+     * @return this instance.
+     * @since 5.7
+     */
+    @Internal
+    public final H2AsyncClientBuilder setSocketChannelFactory(final IOFunction<SocketAddress, SocketChannel> socketChannelFactory) {
+        this.socketChannelFactory = socketChannelFactory;
+        return this;
+    }
+
     /**
      * Sets {@link IOSessionListener} listener.
      *
@@ -914,7 +931,8 @@ public CloseableHttpAsyncClient build() {
                 ioSessionDecorator != null ? ioSessionDecorator : LoggingIOSessionDecorator.INSTANCE,
                 ioReactorExceptionCallback != null ? ioReactorExceptionCallback : LoggingExceptionCallback.INSTANCE,
                 ioSessionListener,
-                ioSession -> ioSession.enqueue(new ShutdownCommand(CloseMode.GRACEFUL), Command.Priority.IMMEDIATE));
+                ioSession -> ioSession.enqueue(new ShutdownCommand(CloseMode.GRACEFUL), Command.Priority.IMMEDIATE),
+                socketChannelFactory);
 
         if (execInterceptors != null) {
             for (final ExecInterceptorEntry entry: execInterceptors) {
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java
index 2e75896e8..82b38fa86 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java
@@ -29,6 +29,8 @@
 
 import java.io.Closeable;
 import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.nio.channels.SocketChannel;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedHashMap;
@@ -115,6 +117,7 @@
 import org.apache.hc.core5.http2.protocol.H2RequestContent;
 import org.apache.hc.core5.http2.protocol.H2RequestTargetHost;
 import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.io.IOFunction;
 import org.apache.hc.core5.pool.ConnPoolControl;
 import org.apache.hc.core5.reactor.Command;
 import org.apache.hc.core5.reactor.DefaultConnectingIOReactor;
@@ -201,6 +204,7 @@ private ExecInterceptorEntry(
     private AsyncClientConnectionManager connManager;
     private boolean connManagerShared;
     private IOReactorConfig ioReactorConfig;
+    private IOFunction<SocketAddress, SocketChannel> socketChannelFactory;
     private IOSessionListener ioSessionListener;
     private Callback<Exception> ioReactorExceptionCallback;
     private Http1Config h1Config;
@@ -347,6 +351,19 @@ public final HttpAsyncClientBuilder setIOReactorConfig(final IOReactorConfig ioR
         return this;
     }
 
+    /**
+     * Sets the factory used by the I/O reactor to create socket channels for
+     * outgoing connections.
+     *
+     * @return this instance.
+     * @since 5.7
+     */
+    @Internal
+    public final HttpAsyncClientBuilder setSocketChannelFactory(final IOFunction<SocketAddress, SocketChannel> socketChannelFactory) {
+        this.socketChannelFactory = socketChannelFactory;
+        return this;
+    }
+
     /**
      * Sets {@link IOSessionListener} listener.
      *
@@ -1204,7 +1221,8 @@ public CloseableHttpAsyncClient build() {
                 ioSessionDecorator != null ? ioSessionDecorator : LoggingIOSessionDecorator.INSTANCE,
                 ioReactorExceptionCallback != null ? ioReactorExceptionCallback : LoggingExceptionCallback.INSTANCE,
                 ioSessionListener,
-                ioSession -> ioSession.enqueue(new ShutdownCommand(CloseMode.GRACEFUL), Command.Priority.IMMEDIATE));
+                ioSession -> ioSession.enqueue(new ShutdownCommand(CloseMode.GRACEFUL), Command.Priority.IMMEDIATE),
+                socketChannelFactory);
 
         if (execInterceptors != null) {
             for (final ExecInterceptorEntry entry: execInterceptors) {
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManagerBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManagerBuilder.java
index 651c14856..5256d3203 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManagerBuilder.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManagerBuilder.java
@@ -34,6 +34,7 @@
 import org.apache.hc.client5.http.SchemePortResolver;
 import org.apache.hc.client5.http.config.ConnectionConfig;
 import org.apache.hc.client5.http.config.TlsConfig;
+import org.apache.hc.client5.http.io.DetachedSocketFactory;
 import org.apache.hc.client5.http.io.HttpClientConnectionOperator;
 import org.apache.hc.client5.http.io.ManagedHttpClientConnection;
 import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
@@ -60,6 +61,7 @@
 public class PoolingHttpClientConnectionManagerBuilder {
 
     private HttpConnectionFactory<ManagedHttpClientConnection> connectionFactory;
+    private DetachedSocketFactory detachedSocketFactory;
     private TlsSocketStrategy tlsSocketStrategy;
     private SchemePortResolver schemePortResolver;
     private DnsResolver dnsResolver;
@@ -96,6 +98,19 @@ public final PoolingHttpClientConnectionManagerBuilder setConnectionFactory(
         return this;
     }
 
+    /**
+     * Sets the factory used to create detached sockets for outgoing connections.
+     *
+     * @return this instance.
+     * @since 5.7
+     */
+    @Internal
+    public final PoolingHttpClientConnectionManagerBuilder setDetachedSocketFactory(
+            final DetachedSocketFactory detachedSocketFactory) {
+        this.detachedSocketFactory = detachedSocketFactory;
+        return this;
+    }
+
     /**
      * Sets {@link org.apache.hc.client5.http.socket.LayeredConnectionSocketFactory} instance.
      *
@@ -325,7 +340,10 @@ protected HttpClientConnectionOperator createConnectionOperator(
             final SchemePortResolver schemePortResolver,
             final DnsResolver dnsResolver,
             final TlsSocketStrategy tlsSocketStrategy) {
-        return new DefaultHttpClientConnectionOperator(schemePortResolver, dnsResolver,
+        return new DefaultHttpClientConnectionOperator(
+                detachedSocketFactory != null ? detachedSocketFactory : DefaultHttpClientConnectionOperator.PLAIN_SOCKET_FACTORY,
+                schemePortResolver,
+                dnsResolver,
                 RegistryBuilder.<TlsSocketStrategy>create()
                         .register(URIScheme.HTTPS.id, tlsSocketStrategy)
                         .build());
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/CustomTransportBuilderTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/CustomTransportBuilderTest.java
new file mode 100644
index 000000000..323548e6e
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/CustomTransportBuilderTest.java
@@ -0,0 +1,72 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.async;
+
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.nio.channels.SocketChannel;
+
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.io.DetachedSocketFactory;
+import org.apache.hc.core5.io.IOFunction;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class CustomTransportBuilderTest {
+
+    @Test
+    void testClassicBuilderAcceptsDetachedSocketFactory() {
+        final DetachedSocketFactory socketFactory = proxy -> new Socket();
+        final PoolingHttpClientConnectionManager connectionManager =
+                PoolingHttpClientConnectionManagerBuilder.create()
+                        .setDetachedSocketFactory(socketFactory)
+                        .build();
+        try {
+            Assertions.assertNotNull(connectionManager);
+        } finally {
+            connectionManager.close();
+        }
+    }
+
+    @Test
+    void testAsyncBuildersAcceptSocketChannelFactory() throws Exception {
+        final IOFunction<SocketAddress, SocketChannel> socketChannelFactory = remoteAddress -> SocketChannel.open();
+        try (CloseableHttpAsyncClient client = HttpAsyncClientBuilder.create()
+                .setSocketChannelFactory(socketChannelFactory)
+                .build()) {
+            Assertions.assertNotNull(client);
+        }
+        try (CloseableHttpAsyncClient client = H2AsyncClientBuilder.create()
+                .setSocketChannelFactory(socketChannelFactory)
+                .build()) {
+            Assertions.assertNotNull(client);
+        }
+    }
+
+}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/SocketChannelFactoryBuilderTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/SocketChannelFactoryBuilderTest.java
new file mode 100644
index 000000000..1f1eb018b
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/SocketChannelFactoryBuilderTest.java
@@ -0,0 +1,88 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.async;
+
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
+import org.apache.hc.core5.io.IOFunction;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class SocketChannelFactoryBuilderTest {
+
+    @Test
+    void testHttpAsyncClientBuilderUsesSocketChannelFactory() throws Exception {
+        final AtomicInteger invocationCount = new AtomicInteger();
+        final IOException expected = new IOException("custom transport");
+        final IOFunction<SocketAddress, SocketChannel> socketChannelFactory = remoteAddress -> {
+            invocationCount.incrementAndGet();
+            throw expected;
+        };
+        try (CloseableHttpAsyncClient client = HttpAsyncClientBuilder.create()
+                .setSocketChannelFactory(socketChannelFactory)
+                .disableAutomaticRetries()
+                .build()) {
+            assertFactoryUsed(client, invocationCount);
+        }
+    }
+
+    @Test
+    void testH2AsyncClientBuilderUsesSocketChannelFactory() throws Exception {
+        final AtomicInteger invocationCount = new AtomicInteger();
+        final IOException expected = new IOException("custom transport");
+        final IOFunction<SocketAddress, SocketChannel> socketChannelFactory = remoteAddress -> {
+            invocationCount.incrementAndGet();
+            throw expected;
+        };
+        try (CloseableHttpAsyncClient client = H2AsyncClientBuilder.create()
+                .setSocketChannelFactory(socketChannelFactory)
+                .disableAutomaticRetries()
+                .build()) {
+            assertFactoryUsed(client, invocationCount);
+        }
+    }
+
+    private static void assertFactoryUsed(
+            final CloseableHttpAsyncClient client,
+            final AtomicInteger invocationCount) throws Exception {
+        client.start();
+        final Future<SimpleHttpResponse> future = client.execute(
+                SimpleRequestBuilder.get("http://127.0.0.1:18080/").build(), null);
+        Assertions.assertThrows(ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS));
+        Assertions.assertEquals(1, invocationCount.get());
+    }
+
+}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/DetachedSocketFactoryBuilderTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/DetachedSocketFactoryBuilderTest.java
new file mode 100644
index 000000000..dda6d43f1
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/DetachedSocketFactoryBuilderTest.java
@@ -0,0 +1,64 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.io;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.io.DetachedSocketFactory;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class DetachedSocketFactoryBuilderTest {
+
+    @Test
+    void testPoolingBuilderUsesDetachedSocketFactory() throws Exception {
+        final AtomicInteger invocationCount = new AtomicInteger();
+        final IOException expected = new IOException("custom transport");
+        final DetachedSocketFactory socketFactory = proxy -> {
+            invocationCount.incrementAndGet();
+            throw expected;
+        };
+        final PoolingHttpClientConnectionManager connectionManager =
+                PoolingHttpClientConnectionManagerBuilder.create()
+                        .setDetachedSocketFactory(socketFactory)
+                        .build();
+        try (CloseableHttpClient client = HttpClients.custom()
+                .setConnectionManager(connectionManager)
+                .disableAutomaticRetries()
+                .build()) {
+            Assertions.assertThrows(IOException.class, () ->
+                    client.executeOpen(null, new HttpGet("http://localhost:18080/"), null));
+            Assertions.assertEquals(1, invocationCount.get());
+        }
+    }
+
+}