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());
+ }
+ }
+
+}