Skip to content

Commit 4722f1e

Browse files
doom369Copilothyperxpro
authored
Add pluggable DnsNameResolver (#2163)
Fixes #1724. Preserve the previous behaviour + ability to plug own DNS name resolver. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: doom369 <1536494+doom369@users.noreply.github.com> Co-authored-by: Aayush Atharva <24762260+hyperxpro@users.noreply.github.com>
1 parent 5b6d5d2 commit 4722f1e

File tree

6 files changed

+376
-36
lines changed

6 files changed

+376
-36
lines changed

client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.netty.channel.ChannelOption;
2222
import io.netty.channel.EventLoopGroup;
2323
import io.netty.handler.ssl.SslContext;
24+
import io.netty.resolver.AddressResolverGroup;
2425
import io.netty.util.HashedWheelTimer;
2526
import io.netty.util.Timer;
2627
import org.asynchttpclient.channel.ChannelPool;
@@ -37,6 +38,7 @@
3738
import org.jetbrains.annotations.Nullable;
3839

3940
import java.io.IOException;
41+
import java.net.InetSocketAddress;
4042
import java.time.Duration;
4143
import java.util.List;
4244
import java.util.Map;
@@ -375,6 +377,26 @@ default boolean isHttp2CleartextEnabled() {
375377
@Nullable
376378
EventLoopGroup getEventLoopGroup();
377379

380+
/**
381+
* Return the {@link AddressResolverGroup} used for asynchronous DNS resolution.
382+
* <p>
383+
* When non-null, this resolver group is used for hostname resolution instead of
384+
* the per-request {@link io.netty.resolver.NameResolver}. For example,
385+
* Netty's {@link io.netty.resolver.dns.DnsAddressResolverGroup} provides
386+
* non-blocking DNS lookups with inflight coalescing across concurrent requests for
387+
* the same hostname.
388+
* <p>
389+
* By default this returns {@code null}, preserving the legacy per-request name
390+
* resolver behavior. Set via
391+
* {@link DefaultAsyncHttpClientConfig.Builder#setAddressResolverGroup(AddressResolverGroup)}.
392+
*
393+
* @return the {@link AddressResolverGroup} or {@code null} to use per-request resolvers
394+
*/
395+
@Nullable
396+
default AddressResolverGroup<InetSocketAddress> getAddressResolverGroup() {
397+
return null;
398+
}
399+
378400
boolean isUseNativeTransport();
379401

380402
boolean isUseOnlyEpollNativeTransport();

client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.netty.channel.ChannelOption;
2121
import io.netty.channel.EventLoopGroup;
2222
import io.netty.handler.ssl.SslContext;
23+
import io.netty.resolver.AddressResolverGroup;
2324
import io.netty.util.Timer;
2425
import org.asynchttpclient.channel.ChannelPool;
2526
import org.asynchttpclient.channel.DefaultKeepAliveStrategy;
@@ -36,6 +37,7 @@
3637
import org.asynchttpclient.util.ProxyUtils;
3738
import org.jetbrains.annotations.Nullable;
3839

40+
import java.net.InetSocketAddress;
3941
import java.time.Duration;
4042
import java.util.Collections;
4143
import java.util.HashMap;
@@ -200,6 +202,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig {
200202
private final int chunkedFileChunkSize;
201203
private final Map<ChannelOption<Object>, Object> channelOptions;
202204
private final @Nullable EventLoopGroup eventLoopGroup;
205+
private final @Nullable AddressResolverGroup<InetSocketAddress> addressResolverGroup;
203206
private final boolean useNativeTransport;
204207
private final boolean useOnlyEpollNativeTransport;
205208
private final @Nullable ByteBufAllocator allocator;
@@ -305,6 +308,7 @@ private DefaultAsyncHttpClientConfig(// http
305308
int webSocketMaxFrameSize,
306309
Map<ChannelOption<Object>, Object> channelOptions,
307310
@Nullable EventLoopGroup eventLoopGroup,
311+
@Nullable AddressResolverGroup<InetSocketAddress> addressResolverGroup,
308312
boolean useNativeTransport,
309313
boolean useOnlyEpollNativeTransport,
310314
@Nullable ByteBufAllocator allocator,
@@ -406,6 +410,7 @@ private DefaultAsyncHttpClientConfig(// http
406410
this.chunkedFileChunkSize = chunkedFileChunkSize;
407411
this.channelOptions = channelOptions;
408412
this.eventLoopGroup = eventLoopGroup;
413+
this.addressResolverGroup = addressResolverGroup;
409414
this.useNativeTransport = useNativeTransport;
410415
this.useOnlyEpollNativeTransport = useOnlyEpollNativeTransport;
411416

@@ -806,6 +811,11 @@ public Map<ChannelOption<Object>, Object> getChannelOptions() {
806811
return eventLoopGroup;
807812
}
808813

814+
@Override
815+
public @Nullable AddressResolverGroup<InetSocketAddress> getAddressResolverGroup() {
816+
return addressResolverGroup;
817+
}
818+
809819
@Override
810820
public boolean isUseNativeTransport() {
811821
return useNativeTransport;
@@ -959,6 +969,7 @@ public static class Builder {
959969
private @Nullable ByteBufAllocator allocator;
960970
private final Map<ChannelOption<Object>, Object> channelOptions = new HashMap<>();
961971
private @Nullable EventLoopGroup eventLoopGroup;
972+
private @Nullable AddressResolverGroup<InetSocketAddress> addressResolverGroup;
962973
private @Nullable Timer nettyTimer;
963974
private @Nullable ThreadFactory threadFactory;
964975
private @Nullable Consumer<Channel> httpAdditionalChannelInitializer;
@@ -1061,6 +1072,7 @@ public Builder(AsyncHttpClientConfig config) {
10611072
chunkedFileChunkSize = config.getChunkedFileChunkSize();
10621073
channelOptions.putAll(config.getChannelOptions());
10631074
eventLoopGroup = config.getEventLoopGroup();
1075+
addressResolverGroup = config.getAddressResolverGroup();
10641076
useNativeTransport = config.isUseNativeTransport();
10651077
useOnlyEpollNativeTransport = config.isUseOnlyEpollNativeTransport();
10661078

@@ -1514,6 +1526,25 @@ public Builder setEventLoopGroup(EventLoopGroup eventLoopGroup) {
15141526
return this;
15151527
}
15161528

1529+
/**
1530+
* Set a custom {@link AddressResolverGroup} for asynchronous DNS resolution.
1531+
* <p>
1532+
* When set, this resolver group is used instead of the per-request {@link io.netty.resolver.NameResolver}.
1533+
* Pass {@code null} (the default) to use per-request resolvers (legacy behavior).
1534+
* <p>
1535+
* <b>Lifecycle:</b> The client takes ownership of the provided resolver group and will
1536+
* {@linkplain AddressResolverGroup#close() close} it when the client is shut down.
1537+
* Do not pass a shared resolver group that is used by other clients unless you manage
1538+
* its lifecycle independently.
1539+
*
1540+
* @param addressResolverGroup the resolver group, or {@code null} to use per-request resolvers
1541+
* @return the same builder instance
1542+
*/
1543+
public Builder setAddressResolverGroup(@Nullable AddressResolverGroup<InetSocketAddress> addressResolverGroup) {
1544+
this.addressResolverGroup = addressResolverGroup;
1545+
return this;
1546+
}
1547+
15171548
public Builder setUseNativeTransport(boolean useNativeTransport) {
15181549
this.useNativeTransport = useNativeTransport;
15191550
return this;
@@ -1650,6 +1681,7 @@ public DefaultAsyncHttpClientConfig build() {
16501681
webSocketMaxFrameSize,
16511682
channelOptions.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(channelOptions),
16521683
eventLoopGroup,
1684+
addressResolverGroup,
16531685
useNativeTransport,
16541686
useOnlyEpollNativeTransport,
16551687
allocator,

client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java

Lines changed: 70 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
import io.netty.handler.ssl.SslHandler;
5454
import io.netty.handler.stream.ChunkedWriteHandler;
5555
import io.netty.handler.timeout.IdleStateHandler;
56+
import io.netty.resolver.AddressResolver;
57+
import io.netty.resolver.AddressResolverGroup;
5658
import io.netty.resolver.NameResolver;
5759
import io.netty.util.Timer;
5860
import io.netty.util.concurrent.DefaultThreadFactory;
@@ -82,6 +84,7 @@
8284
import org.asynchttpclient.proxy.ProxyServer;
8385
import org.asynchttpclient.proxy.ProxyType;
8486
import org.asynchttpclient.uri.Uri;
87+
import org.jetbrains.annotations.Nullable;
8588
import org.slf4j.Logger;
8689
import org.slf4j.LoggerFactory;
8790

@@ -122,6 +125,7 @@ public class ChannelManager {
122125
private final Bootstrap httpBootstrap;
123126
private final Bootstrap wsBootstrap;
124127
private final long handshakeTimeout;
128+
private final @Nullable AddressResolverGroup<InetSocketAddress> addressResolverGroup;
125129

126130
private final ChannelPool channelPool;
127131
private final ChannelGroup openChannels;
@@ -193,6 +197,9 @@ public ChannelManager(final AsyncHttpClientConfig config, Timer nettyTimer) {
193197

194198
httpBootstrap = newBootstrap(transportFactory, eventLoopGroup, config);
195199
wsBootstrap = newBootstrap(transportFactory, eventLoopGroup, config);
200+
201+
// Use the address resolver group from config if provided; otherwise null (legacy per-request resolution)
202+
addressResolverGroup = config.getAddressResolverGroup();
196203
}
197204

198205
private static TransportFactory<? extends Channel, ? extends EventLoopGroup> getNativeTransportFactory(AsyncHttpClientConfig config) {
@@ -412,6 +419,11 @@ private void doClose() {
412419
}
413420

414421
public void close() {
422+
// Close the resolver group first while the EventLoopGroup is still active,
423+
// since Netty DNS resolvers may need a live EventLoop for clean shutdown.
424+
if (addressResolverGroup != null) {
425+
addressResolverGroup.close();
426+
}
415427
if (allowReleaseEventLoopGroup) {
416428
final long shutdownQuietPeriod = config.getShutdownQuietPeriod().toMillis();
417429
final long shutdownTimeout = config.getShutdownTimeout().toMillis();
@@ -579,39 +591,27 @@ public Future<Bootstrap> getBootstrap(Uri uri, NameResolver<InetAddress> nameRes
579591
Bootstrap socksBootstrap = httpBootstrap.clone();
580592
ChannelHandler httpBootstrapHandler = socksBootstrap.config().handler();
581593

582-
nameResolver.resolve(proxy.getHost()).addListener((Future<InetAddress> whenProxyAddress) -> {
583-
if (whenProxyAddress.isSuccess()) {
584-
socksBootstrap.handler(new ChannelInitializer<Channel>() {
585-
@Override
586-
protected void initChannel(Channel channel) throws Exception {
587-
channel.pipeline().addLast(httpBootstrapHandler);
588-
589-
InetSocketAddress proxyAddress = new InetSocketAddress(whenProxyAddress.get(), proxy.getPort());
590-
Realm realm = proxy.getRealm();
591-
String username = realm != null ? realm.getPrincipal() : null;
592-
String password = realm != null ? realm.getPassword() : null;
593-
ProxyHandler socksProxyHandler;
594-
switch (proxy.getProxyType()) {
595-
case SOCKS_V4:
596-
socksProxyHandler = new Socks4ProxyHandler(proxyAddress, username);
597-
break;
598-
599-
case SOCKS_V5:
600-
socksProxyHandler = new Socks5ProxyHandler(proxyAddress, username, password);
601-
break;
602-
603-
default:
604-
throw new IllegalArgumentException("Only SOCKS4 and SOCKS5 supported at the moment.");
605-
}
606-
channel.pipeline().addFirst(SOCKS_HANDLER, socksProxyHandler);
607-
}
608-
});
609-
promise.setSuccess(socksBootstrap);
610-
611-
} else {
612-
promise.setFailure(whenProxyAddress.cause());
613-
}
614-
});
594+
if (addressResolverGroup != null) {
595+
// Use the address resolver group for async, non-blocking proxy host resolution
596+
InetSocketAddress unresolvedProxyAddress = InetSocketAddress.createUnresolved(proxy.getHost(), proxy.getPort());
597+
AddressResolver<InetSocketAddress> resolver = addressResolverGroup.getResolver(eventLoopGroup.next());
598+
resolver.resolve(unresolvedProxyAddress).addListener((Future<InetSocketAddress> whenProxyAddress) -> {
599+
if (whenProxyAddress.isSuccess()) {
600+
configureSocksBootstrap(socksBootstrap, httpBootstrapHandler, whenProxyAddress.get(), proxy, promise);
601+
} else {
602+
promise.setFailure(whenProxyAddress.cause());
603+
}
604+
});
605+
} else {
606+
nameResolver.resolve(proxy.getHost()).addListener((Future<InetAddress> whenProxyAddress) -> {
607+
if (whenProxyAddress.isSuccess()) {
608+
InetSocketAddress proxyAddress = new InetSocketAddress(whenProxyAddress.get(), proxy.getPort());
609+
configureSocksBootstrap(socksBootstrap, httpBootstrapHandler, proxyAddress, proxy, promise);
610+
} else {
611+
promise.setFailure(whenProxyAddress.cause());
612+
}
613+
});
614+
}
615615

616616
} else if (proxy != null && ProxyType.HTTPS.equals(proxy.getProxyType())) {
617617
// For HTTPS proxies, use HTTP bootstrap but ensure SSL connection to proxy
@@ -624,6 +624,35 @@ protected void initChannel(Channel channel) throws Exception {
624624
return promise;
625625
}
626626

627+
private void configureSocksBootstrap(Bootstrap socksBootstrap, ChannelHandler httpBootstrapHandler,
628+
InetSocketAddress proxyAddress, ProxyServer proxy, Promise<Bootstrap> promise) {
629+
socksBootstrap.handler(new ChannelInitializer<Channel>() {
630+
@Override
631+
protected void initChannel(Channel channel) throws Exception {
632+
channel.pipeline().addLast(httpBootstrapHandler);
633+
634+
Realm realm = proxy.getRealm();
635+
String username = realm != null ? realm.getPrincipal() : null;
636+
String password = realm != null ? realm.getPassword() : null;
637+
ProxyHandler socksProxyHandler;
638+
switch (proxy.getProxyType()) {
639+
case SOCKS_V4:
640+
socksProxyHandler = new Socks4ProxyHandler(proxyAddress, username);
641+
break;
642+
643+
case SOCKS_V5:
644+
socksProxyHandler = new Socks5ProxyHandler(proxyAddress, username, password);
645+
break;
646+
647+
default:
648+
throw new IllegalArgumentException("Only SOCKS4 and SOCKS5 supported at the moment.");
649+
}
650+
channel.pipeline().addFirst(SOCKS_HANDLER, socksProxyHandler);
651+
}
652+
});
653+
promise.setSuccess(socksBootstrap);
654+
}
655+
627656
/**
628657
* Checks whether the given channel is an HTTP/2 connection (i.e. has the HTTP/2 multiplex handler installed).
629658
*/
@@ -790,6 +819,14 @@ public EventLoopGroup getEventLoopGroup() {
790819
return eventLoopGroup;
791820
}
792821

822+
/**
823+
* Return the {@link AddressResolverGroup} used for async DNS resolution, or {@code null}
824+
* if per-request name resolvers should be used (legacy behavior).
825+
*/
826+
public @Nullable AddressResolverGroup<InetSocketAddress> getAddressResolverGroup() {
827+
return addressResolverGroup;
828+
}
829+
793830
public ClientStats getClientStats() {
794831
Map<String, Long> totalConnectionsPerHost = openChannels.stream()
795832
.map(Channel::remoteAddress)

client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import io.netty.handler.codec.http2.Http2Headers;
3535
import io.netty.handler.codec.http2.Http2StreamChannel;
3636
import io.netty.handler.codec.http2.Http2StreamChannelBootstrap;
37+
import io.netty.resolver.AddressResolver;
38+
import io.netty.resolver.AddressResolverGroup;
3739
import io.netty.util.ReferenceCountUtil;
3840
import io.netty.util.Timer;
3941
import io.netty.util.concurrent.Future;
@@ -72,6 +74,7 @@
7274
import org.asynchttpclient.resolver.RequestHostnameResolver;
7375
import org.asynchttpclient.uri.Uri;
7476
import org.asynchttpclient.ws.WebSocketUpgradeHandler;
77+
7578
import org.slf4j.Logger;
7679
import org.slf4j.LoggerFactory;
7780

@@ -374,7 +377,7 @@ private <T> Future<List<InetSocketAddress>> resolveAddresses(Request request, Pr
374377
int port = ProxyType.HTTPS.equals(proxy.getProxyType()) || uri.isSecured() ? proxy.getSecuredPort() : proxy.getPort();
375378
InetSocketAddress unresolvedRemoteAddress = InetSocketAddress.createUnresolved(proxy.getHost(), port);
376379
scheduleRequestTimeout(future, unresolvedRemoteAddress);
377-
return RequestHostnameResolver.INSTANCE.resolve(request.getNameResolver(), unresolvedRemoteAddress, asyncHandler);
380+
return resolveHostname(request, unresolvedRemoteAddress, asyncHandler);
378381
} else {
379382
int port = uri.getExplicitPort();
380383

@@ -385,10 +388,18 @@ private <T> Future<List<InetSocketAddress>> resolveAddresses(Request request, Pr
385388
// bypass resolution
386389
InetSocketAddress inetSocketAddress = new InetSocketAddress(request.getAddress(), port);
387390
return promise.setSuccess(singletonList(inetSocketAddress));
388-
} else {
389-
return RequestHostnameResolver.INSTANCE.resolve(request.getNameResolver(), unresolvedRemoteAddress, asyncHandler);
390391
}
392+
return resolveHostname(request, unresolvedRemoteAddress, asyncHandler);
393+
}
394+
}
395+
396+
private Future<List<InetSocketAddress>> resolveHostname(Request request, InetSocketAddress unresolvedRemoteAddress, AsyncHandler<?> asyncHandler) {
397+
AddressResolverGroup<InetSocketAddress> group = channelManager.getAddressResolverGroup();
398+
if (group != null) {
399+
AddressResolver<InetSocketAddress> resolver = group.getResolver(channelManager.getEventLoopGroup().next());
400+
return RequestHostnameResolver.INSTANCE.resolve(resolver, unresolvedRemoteAddress, asyncHandler);
391401
}
402+
return RequestHostnameResolver.INSTANCE.resolve(request.getNameResolver(), unresolvedRemoteAddress, asyncHandler);
392403
}
393404

394405
private <T> NettyResponseFuture<T> newNettyResponseFuture(Request request, AsyncHandler<T> asyncHandler, NettyRequest nettyRequest, ProxyServer proxyServer) {

0 commit comments

Comments
 (0)