5353import io .netty .handler .ssl .SslHandler ;
5454import io .netty .handler .stream .ChunkedWriteHandler ;
5555import io .netty .handler .timeout .IdleStateHandler ;
56+ import io .netty .resolver .AddressResolver ;
57+ import io .netty .resolver .AddressResolverGroup ;
5658import io .netty .resolver .NameResolver ;
5759import io .netty .util .Timer ;
5860import io .netty .util .concurrent .DefaultThreadFactory ;
8284import org .asynchttpclient .proxy .ProxyServer ;
8385import org .asynchttpclient .proxy .ProxyType ;
8486import org .asynchttpclient .uri .Uri ;
87+ import org .jetbrains .annotations .Nullable ;
8588import org .slf4j .Logger ;
8689import 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 )
0 commit comments