Skip to content

Commit 60b2182

Browse files
committed
HTTPCLIENT-2429: Improve support for custom transport layers
Expose supported custom transport extension points for classic and async clients.
1 parent 86c313b commit 60b2182

7 files changed

Lines changed: 273 additions & 5 deletions

File tree

httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
import org.apache.hc.core5.reactor.IOReactorConfig;
104104
import org.apache.hc.core5.reactor.IOSession;
105105
import org.apache.hc.core5.reactor.IOSessionListener;
106+
import org.apache.hc.core5.reactor.SocketChannelFactory;
106107
import org.apache.hc.core5.util.Args;
107108
import org.apache.hc.core5.util.TimeValue;
108109
import org.apache.hc.core5.util.VersionInfo;
@@ -172,6 +173,7 @@ private ExecInterceptorEntry(
172173
}
173174

174175
private IOReactorConfig ioReactorConfig;
176+
private SocketChannelFactory socketChannelFactory;
175177
private IOSessionListener ioSessionListener;
176178
private H2Config h2Config;
177179
private CharCodingConfig charCodingConfig;
@@ -249,6 +251,18 @@ public final H2AsyncClientBuilder setIOReactorConfig(final IOReactorConfig ioRea
249251
return this;
250252
}
251253

254+
/**
255+
* Sets the factory used by the I/O reactor to create socket channels for
256+
* outgoing connections.
257+
*
258+
* @return this instance.
259+
* @since 5.7
260+
*/
261+
public final H2AsyncClientBuilder setSocketChannelFactory(final SocketChannelFactory socketChannelFactory) {
262+
this.socketChannelFactory = socketChannelFactory;
263+
return this;
264+
}
265+
252266
/**
253267
* Sets {@link IOSessionListener} listener.
254268
*
@@ -906,7 +920,8 @@ public CloseableHttpAsyncClient build() {
906920
ioSessionDecorator != null ? ioSessionDecorator : LoggingIOSessionDecorator.INSTANCE,
907921
ioReactorExceptionCallback != null ? ioReactorExceptionCallback : LoggingExceptionCallback.INSTANCE,
908922
ioSessionListener,
909-
ioSession -> ioSession.enqueue(new ShutdownCommand(CloseMode.GRACEFUL), Command.Priority.IMMEDIATE));
923+
ioSession -> ioSession.enqueue(new ShutdownCommand(CloseMode.GRACEFUL), Command.Priority.IMMEDIATE),
924+
socketChannelFactory);
910925

911926
if (execInterceptors != null) {
912927
for (final ExecInterceptorEntry entry: execInterceptors) {

httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
import org.apache.hc.core5.reactor.IOReactorConfig;
122122
import org.apache.hc.core5.reactor.IOSession;
123123
import org.apache.hc.core5.reactor.IOSessionListener;
124+
import org.apache.hc.core5.reactor.SocketChannelFactory;
124125
import org.apache.hc.core5.util.Args;
125126
import org.apache.hc.core5.util.TimeValue;
126127
import org.apache.hc.core5.util.VersionInfo;
@@ -200,6 +201,7 @@ private ExecInterceptorEntry(
200201
private AsyncClientConnectionManager connManager;
201202
private boolean connManagerShared;
202203
private IOReactorConfig ioReactorConfig;
204+
private SocketChannelFactory socketChannelFactory;
203205
private IOSessionListener ioSessionListener;
204206
private Callback<Exception> ioReactorExceptionCallback;
205207
private Http1Config h1Config;
@@ -346,6 +348,18 @@ public final HttpAsyncClientBuilder setIOReactorConfig(final IOReactorConfig ioR
346348
return this;
347349
}
348350

351+
/**
352+
* Sets the factory used by the I/O reactor to create socket channels for
353+
* outgoing connections.
354+
*
355+
* @return this instance.
356+
* @since 5.7
357+
*/
358+
public final HttpAsyncClientBuilder setSocketChannelFactory(final SocketChannelFactory socketChannelFactory) {
359+
this.socketChannelFactory = socketChannelFactory;
360+
return this;
361+
}
362+
349363
/**
350364
* Sets {@link IOSessionListener} listener.
351365
*
@@ -1197,7 +1211,8 @@ public CloseableHttpAsyncClient build() {
11971211
ioSessionDecorator != null ? ioSessionDecorator : LoggingIOSessionDecorator.INSTANCE,
11981212
ioReactorExceptionCallback != null ? ioReactorExceptionCallback : LoggingExceptionCallback.INSTANCE,
11991213
ioSessionListener,
1200-
ioSession -> ioSession.enqueue(new ShutdownCommand(CloseMode.GRACEFUL), Command.Priority.IMMEDIATE));
1214+
ioSession -> ioSession.enqueue(new ShutdownCommand(CloseMode.GRACEFUL), Command.Priority.IMMEDIATE),
1215+
socketChannelFactory);
12011216

12021217
if (execInterceptors != null) {
12031218
for (final ExecInterceptorEntry entry: execInterceptors) {

httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManagerBuilder.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.apache.hc.client5.http.SchemePortResolver;
3535
import org.apache.hc.client5.http.config.ConnectionConfig;
3636
import org.apache.hc.client5.http.config.TlsConfig;
37+
import org.apache.hc.client5.http.io.DetachedSocketFactory;
3738
import org.apache.hc.client5.http.io.HttpClientConnectionOperator;
3839
import org.apache.hc.client5.http.io.ManagedHttpClientConnection;
3940
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
@@ -60,6 +61,7 @@
6061
public class PoolingHttpClientConnectionManagerBuilder {
6162

6263
private HttpConnectionFactory<ManagedHttpClientConnection> connectionFactory;
64+
private DetachedSocketFactory detachedSocketFactory;
6365
private TlsSocketStrategy tlsSocketStrategy;
6466
private SchemePortResolver schemePortResolver;
6567
private DnsResolver dnsResolver;
@@ -96,6 +98,18 @@ public final PoolingHttpClientConnectionManagerBuilder setConnectionFactory(
9698
return this;
9799
}
98100

101+
/**
102+
* Sets the factory used to create detached sockets for outgoing connections.
103+
*
104+
* @return this instance.
105+
* @since 5.7
106+
*/
107+
public final PoolingHttpClientConnectionManagerBuilder setDetachedSocketFactory(
108+
final DetachedSocketFactory detachedSocketFactory) {
109+
this.detachedSocketFactory = detachedSocketFactory;
110+
return this;
111+
}
112+
99113
/**
100114
* Sets {@link org.apache.hc.client5.http.socket.LayeredConnectionSocketFactory} instance.
101115
*
@@ -325,7 +339,10 @@ protected HttpClientConnectionOperator createConnectionOperator(
325339
final SchemePortResolver schemePortResolver,
326340
final DnsResolver dnsResolver,
327341
final TlsSocketStrategy tlsSocketStrategy) {
328-
return new DefaultHttpClientConnectionOperator(schemePortResolver, dnsResolver,
342+
return new DefaultHttpClientConnectionOperator(
343+
detachedSocketFactory != null ? detachedSocketFactory : DefaultHttpClientConnectionOperator.PLAIN_SOCKET_FACTORY,
344+
schemePortResolver,
345+
dnsResolver,
329346
RegistryBuilder.<TlsSocketStrategy>create()
330347
.register(URIScheme.HTTPS.id, tlsSocketStrategy)
331348
.build());

httpclient5/src/main/java/org/apache/hc/client5/http/io/DetachedSocketFactory.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@
3232
import java.net.Socket;
3333

3434
import org.apache.hc.core5.annotation.Contract;
35-
import org.apache.hc.core5.annotation.Internal;
3635
import org.apache.hc.core5.annotation.ThreadingBehavior;
3736

3837
/**
38+
* Factory for detached sockets used for outgoing classic connections.
39+
*
3940
* @since 5.4
4041
*/
41-
@Internal
4242
@Contract(threading = ThreadingBehavior.STATELESS)
4343
public interface DetachedSocketFactory {
4444

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
28+
package org.apache.hc.client5.http.impl.async;
29+
30+
import java.net.Socket;
31+
import java.nio.channels.SocketChannel;
32+
33+
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
34+
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
35+
import org.apache.hc.client5.http.io.DetachedSocketFactory;
36+
import org.apache.hc.core5.reactor.SocketChannelFactory;
37+
import org.junit.jupiter.api.Assertions;
38+
import org.junit.jupiter.api.Test;
39+
40+
class CustomTransportBuilderTest {
41+
42+
@Test
43+
void testClassicBuilderAcceptsDetachedSocketFactory() {
44+
final DetachedSocketFactory socketFactory = proxy -> new Socket();
45+
final PoolingHttpClientConnectionManager connectionManager =
46+
PoolingHttpClientConnectionManagerBuilder.create()
47+
.setDetachedSocketFactory(socketFactory)
48+
.build();
49+
try {
50+
Assertions.assertNotNull(connectionManager);
51+
} finally {
52+
connectionManager.close();
53+
}
54+
}
55+
56+
@Test
57+
void testAsyncBuildersAcceptSocketChannelFactory() throws Exception {
58+
final SocketChannelFactory socketChannelFactory = remoteAddress -> SocketChannel.open();
59+
try (CloseableHttpAsyncClient client = HttpAsyncClientBuilder.create()
60+
.setSocketChannelFactory(socketChannelFactory)
61+
.build()) {
62+
Assertions.assertNotNull(client);
63+
}
64+
try (CloseableHttpAsyncClient client = H2AsyncClientBuilder.create()
65+
.setSocketChannelFactory(socketChannelFactory)
66+
.build()) {
67+
Assertions.assertNotNull(client);
68+
}
69+
}
70+
71+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
28+
package org.apache.hc.client5.http.impl.async;
29+
30+
import java.io.IOException;
31+
import java.util.concurrent.ExecutionException;
32+
import java.util.concurrent.Future;
33+
import java.util.concurrent.TimeUnit;
34+
import java.util.concurrent.atomic.AtomicInteger;
35+
36+
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
37+
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
38+
import org.apache.hc.core5.reactor.SocketChannelFactory;
39+
import org.junit.jupiter.api.Assertions;
40+
import org.junit.jupiter.api.Test;
41+
42+
class SocketChannelFactoryBuilderTest {
43+
44+
@Test
45+
void testHttpAsyncClientBuilderUsesSocketChannelFactory() throws Exception {
46+
final AtomicInteger invocationCount = new AtomicInteger();
47+
final IOException expected = new IOException("custom transport");
48+
final SocketChannelFactory socketChannelFactory = remoteAddress -> {
49+
invocationCount.incrementAndGet();
50+
throw expected;
51+
};
52+
try (CloseableHttpAsyncClient client = HttpAsyncClientBuilder.create()
53+
.setSocketChannelFactory(socketChannelFactory)
54+
.disableAutomaticRetries()
55+
.build()) {
56+
assertFactoryUsed(client, invocationCount);
57+
}
58+
}
59+
60+
@Test
61+
void testH2AsyncClientBuilderUsesSocketChannelFactory() throws Exception {
62+
final AtomicInteger invocationCount = new AtomicInteger();
63+
final IOException expected = new IOException("custom transport");
64+
final SocketChannelFactory socketChannelFactory = remoteAddress -> {
65+
invocationCount.incrementAndGet();
66+
throw expected;
67+
};
68+
try (CloseableHttpAsyncClient client = H2AsyncClientBuilder.create()
69+
.setSocketChannelFactory(socketChannelFactory)
70+
.disableAutomaticRetries()
71+
.build()) {
72+
assertFactoryUsed(client, invocationCount);
73+
}
74+
}
75+
76+
private static void assertFactoryUsed(
77+
final CloseableHttpAsyncClient client,
78+
final AtomicInteger invocationCount) throws Exception {
79+
client.start();
80+
final Future<SimpleHttpResponse> future = client.execute(
81+
SimpleRequestBuilder.get("http://localhost:18080/").build(), null);
82+
Assertions.assertThrows(ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS));
83+
Assertions.assertEquals(1, invocationCount.get());
84+
}
85+
86+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
28+
package org.apache.hc.client5.http.impl.io;
29+
30+
import java.io.IOException;
31+
import java.util.concurrent.atomic.AtomicInteger;
32+
33+
import org.apache.hc.client5.http.classic.methods.HttpGet;
34+
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
35+
import org.apache.hc.client5.http.impl.classic.HttpClients;
36+
import org.apache.hc.client5.http.io.DetachedSocketFactory;
37+
import org.junit.jupiter.api.Assertions;
38+
import org.junit.jupiter.api.Test;
39+
40+
class DetachedSocketFactoryBuilderTest {
41+
42+
@Test
43+
void testPoolingBuilderUsesDetachedSocketFactory() throws Exception {
44+
final AtomicInteger invocationCount = new AtomicInteger();
45+
final IOException expected = new IOException("custom transport");
46+
final DetachedSocketFactory socketFactory = proxy -> {
47+
invocationCount.incrementAndGet();
48+
throw expected;
49+
};
50+
final PoolingHttpClientConnectionManager connectionManager =
51+
PoolingHttpClientConnectionManagerBuilder.create()
52+
.setDetachedSocketFactory(socketFactory)
53+
.build();
54+
try (CloseableHttpClient client = HttpClients.custom()
55+
.setConnectionManager(connectionManager)
56+
.disableAutomaticRetries()
57+
.build()) {
58+
Assertions.assertThrows(IOException.class, () ->
59+
client.executeOpen(null, new HttpGet("http://localhost:18080/"), null));
60+
Assertions.assertEquals(1, invocationCount.get());
61+
}
62+
}
63+
64+
}

0 commit comments

Comments
 (0)