From 79a35644134827266f36e67877d1e617ec08e525 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:36:49 +0000 Subject: [PATCH 1/8] Replace blocking DefaultNameResolver with Netty DnsAddressResolverGroup for async DNS - Add AddressResolverGroup config to AsyncHttpClientConfig interface - Add field, getter, builder method to DefaultAsyncHttpClientConfig - ChannelManager creates DnsAddressResolverGroup by default for async DNS with inflight coalescing - Add AddressResolver-based resolution path in RequestHostnameResolver - NettyRequestSender uses group resolver when request uses default resolver - ChannelManager.getBootstrap uses group resolver for SOCKS proxy resolution - Deprecate RequestBuilderBase.DEFAULT_NAME_RESOLVER (kept as sentinel) Agent-Logs-Url: https://github.com/doom369/async-http-client/sessions/b442c622-4c82-440d-84d0-50ee30bf3f27 Co-authored-by: doom369 <1536494+doom369@users.noreply.github.com> --- .../AsyncHttpClientConfig.java | 19 +++++++ .../DefaultAsyncHttpClientConfig.java | 30 ++++++++++ .../asynchttpclient/RequestBuilderBase.java | 9 +++ .../netty/channel/ChannelManager.java | 34 +++++++++++- .../netty/request/NettyRequestSender.java | 13 +++++ .../resolver/RequestHostnameResolver.java | 55 +++++++++++++++++++ 6 files changed, 158 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java index e6e9a1d913..b84413acde 100644 --- a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java @@ -21,6 +21,7 @@ import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.handler.ssl.SslContext; +import io.netty.resolver.AddressResolverGroup; import io.netty.util.HashedWheelTimer; import io.netty.util.Timer; import org.asynchttpclient.channel.ChannelPool; @@ -37,6 +38,7 @@ import org.jetbrains.annotations.Nullable; import java.io.IOException; +import java.net.InetSocketAddress; import java.time.Duration; import java.util.List; import java.util.Map; @@ -375,6 +377,23 @@ default boolean isHttp2CleartextEnabled() { @Nullable EventLoopGroup getEventLoopGroup(); + /** + * Return the {@link AddressResolverGroup} used for asynchronous DNS resolution. + *

+ * When non-null, this resolver group is used for hostname resolution instead of + * the per-request {@link io.netty.resolver.NameResolver}. The default implementation + * uses Netty's {@link io.netty.resolver.dns.DnsAddressResolverGroup} which provides + * non-blocking DNS lookups with inflight coalescing across concurrent requests for + * the same hostname. + *

+ * Providing {@code null} disables the group resolver and falls back to + * per-request name resolvers (legacy behavior). + * + * @return the {@link AddressResolverGroup} or {@code null} to use per-request resolvers + */ + @Nullable + AddressResolverGroup getAddressResolverGroup(); + boolean isUseNativeTransport(); boolean isUseOnlyEpollNativeTransport(); diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java index 4500d0a24f..56f0c0e28d 100644 --- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java @@ -20,6 +20,7 @@ import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.handler.ssl.SslContext; +import io.netty.resolver.AddressResolverGroup; import io.netty.util.Timer; import org.asynchttpclient.channel.ChannelPool; import org.asynchttpclient.channel.DefaultKeepAliveStrategy; @@ -36,6 +37,7 @@ import org.asynchttpclient.util.ProxyUtils; import org.jetbrains.annotations.Nullable; +import java.net.InetSocketAddress; import java.time.Duration; import java.util.Collections; import java.util.HashMap; @@ -200,6 +202,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { private final int chunkedFileChunkSize; private final Map, Object> channelOptions; private final @Nullable EventLoopGroup eventLoopGroup; + private final @Nullable AddressResolverGroup addressResolverGroup; private final boolean useNativeTransport; private final boolean useOnlyEpollNativeTransport; private final @Nullable ByteBufAllocator allocator; @@ -305,6 +308,7 @@ private DefaultAsyncHttpClientConfig(// http int webSocketMaxFrameSize, Map, Object> channelOptions, @Nullable EventLoopGroup eventLoopGroup, + @Nullable AddressResolverGroup addressResolverGroup, boolean useNativeTransport, boolean useOnlyEpollNativeTransport, @Nullable ByteBufAllocator allocator, @@ -406,6 +410,7 @@ private DefaultAsyncHttpClientConfig(// http this.chunkedFileChunkSize = chunkedFileChunkSize; this.channelOptions = channelOptions; this.eventLoopGroup = eventLoopGroup; + this.addressResolverGroup = addressResolverGroup; this.useNativeTransport = useNativeTransport; this.useOnlyEpollNativeTransport = useOnlyEpollNativeTransport; @@ -806,6 +811,11 @@ public Map, Object> getChannelOptions() { return eventLoopGroup; } + @Override + public @Nullable AddressResolverGroup getAddressResolverGroup() { + return addressResolverGroup; + } + @Override public boolean isUseNativeTransport() { return useNativeTransport; @@ -959,6 +969,7 @@ public static class Builder { private @Nullable ByteBufAllocator allocator; private final Map, Object> channelOptions = new HashMap<>(); private @Nullable EventLoopGroup eventLoopGroup; + private @Nullable AddressResolverGroup addressResolverGroup; private @Nullable Timer nettyTimer; private @Nullable ThreadFactory threadFactory; private @Nullable Consumer httpAdditionalChannelInitializer; @@ -1061,6 +1072,7 @@ public Builder(AsyncHttpClientConfig config) { chunkedFileChunkSize = config.getChunkedFileChunkSize(); channelOptions.putAll(config.getChannelOptions()); eventLoopGroup = config.getEventLoopGroup(); + addressResolverGroup = config.getAddressResolverGroup(); useNativeTransport = config.isUseNativeTransport(); useOnlyEpollNativeTransport = config.isUseOnlyEpollNativeTransport(); @@ -1514,6 +1526,23 @@ public Builder setEventLoopGroup(EventLoopGroup eventLoopGroup) { return this; } + /** + * Set a custom {@link AddressResolverGroup} for asynchronous DNS resolution. + *

+ * When set, this resolver group is used instead of the per-request {@link io.netty.resolver.NameResolver}. + * Pass {@code null} to disable the group resolver and fall back to per-request resolvers (legacy behavior). + *

+ * If not explicitly set, a {@link io.netty.resolver.dns.DnsAddressResolverGroup} is created automatically, + * providing non-blocking DNS with inflight coalescing. + * + * @param addressResolverGroup the resolver group, or {@code null} to use per-request resolvers + * @return the same builder instance + */ + public Builder setAddressResolverGroup(@Nullable AddressResolverGroup addressResolverGroup) { + this.addressResolverGroup = addressResolverGroup; + return this; + } + public Builder setUseNativeTransport(boolean useNativeTransport) { this.useNativeTransport = useNativeTransport; return this; @@ -1650,6 +1679,7 @@ public DefaultAsyncHttpClientConfig build() { webSocketMaxFrameSize, channelOptions.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(channelOptions), eventLoopGroup, + addressResolverGroup, useNativeTransport, useOnlyEpollNativeTransport, allocator, diff --git a/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java b/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java index 29bbaa670e..eee9c9cecf 100644 --- a/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java +++ b/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java @@ -60,6 +60,15 @@ public abstract class RequestBuilderBase> { private static final Logger LOGGER = LoggerFactory.getLogger(RequestBuilderBase.class); private static final Uri DEFAULT_REQUEST_URL = Uri.create("http://localhost"); + /** + * @deprecated This blocking resolver is retained only as a sentinel value for backward compatibility. + * When a request uses this default, the client's {@link io.netty.resolver.AddressResolverGroup} + * (typically {@link io.netty.resolver.dns.DnsAddressResolverGroup}) is used instead, providing + * non-blocking DNS with inflight coalescing. To customize DNS resolution, either configure an + * {@link io.netty.resolver.AddressResolverGroup} on the client config or set a custom + * {@link NameResolver} on individual requests. + */ + @Deprecated public static final NameResolver DEFAULT_NAME_RESOLVER = new DefaultNameResolver(ImmediateEventExecutor.INSTANCE); // builder only fields protected UriEncoder uriEncoder; diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java index 0af03dfd41..9547b0526e 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java @@ -53,7 +53,10 @@ import io.netty.handler.ssl.SslHandler; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.handler.timeout.IdleStateHandler; +import io.netty.resolver.AddressResolver; +import io.netty.resolver.AddressResolverGroup; import io.netty.resolver.NameResolver; +import io.netty.resolver.dns.DnsAddressResolverGroup; import io.netty.util.Timer; import io.netty.util.concurrent.DefaultThreadFactory; import io.netty.util.concurrent.Future; @@ -122,6 +125,8 @@ public class ChannelManager { private final Bootstrap httpBootstrap; private final Bootstrap wsBootstrap; private final long handshakeTimeout; + private final AddressResolverGroup addressResolverGroup; + private final boolean allowCloseAddressResolverGroup; private final ChannelPool channelPool; private final ChannelGroup openChannels; @@ -193,6 +198,16 @@ public ChannelManager(final AsyncHttpClientConfig config, Timer nettyTimer) { httpBootstrap = newBootstrap(transportFactory, eventLoopGroup, config); wsBootstrap = newBootstrap(transportFactory, eventLoopGroup, config); + + // Initialize the address resolver group for async DNS + if (config.getAddressResolverGroup() != null) { + addressResolverGroup = config.getAddressResolverGroup(); + allowCloseAddressResolverGroup = false; + } else { + addressResolverGroup = new DnsAddressResolverGroup(io.netty.channel.socket.nio.NioDatagramChannel.class, + io.netty.resolver.dns.DnsServerAddressStreamProviders.platformDefault()); + allowCloseAddressResolverGroup = true; + } } private static TransportFactory getNativeTransportFactory(AsyncHttpClientConfig config) { @@ -409,6 +424,9 @@ private void doClose() { ChannelGroupFuture groupFuture = openChannels.close(); channelPool.destroy(); groupFuture.addListener(future -> sslEngineFactory.destroy()); + if (allowCloseAddressResolverGroup) { + addressResolverGroup.close(); + } } public void close() { @@ -579,14 +597,17 @@ public Future getBootstrap(Uri uri, NameResolver nameRes Bootstrap socksBootstrap = httpBootstrap.clone(); ChannelHandler httpBootstrapHandler = socksBootstrap.config().handler(); - nameResolver.resolve(proxy.getHost()).addListener((Future whenProxyAddress) -> { + // Use the address resolver group for async, non-blocking proxy host resolution + InetSocketAddress unresolvedProxyAddress = InetSocketAddress.createUnresolved(proxy.getHost(), proxy.getPort()); + AddressResolver resolver = addressResolverGroup.getResolver(eventLoopGroup.next()); + resolver.resolve(unresolvedProxyAddress).addListener((Future whenProxyAddress) -> { if (whenProxyAddress.isSuccess()) { socksBootstrap.handler(new ChannelInitializer() { @Override protected void initChannel(Channel channel) throws Exception { channel.pipeline().addLast(httpBootstrapHandler); - InetSocketAddress proxyAddress = new InetSocketAddress(whenProxyAddress.get(), proxy.getPort()); + InetSocketAddress proxyAddress = whenProxyAddress.get(); Realm realm = proxy.getRealm(); String username = realm != null ? realm.getPrincipal() : null; String password = realm != null ? realm.getPassword() : null; @@ -790,6 +811,15 @@ public EventLoopGroup getEventLoopGroup() { return eventLoopGroup; } + /** + * Return the {@link AddressResolverGroup} used for async DNS resolution. + * Resolvers obtained from this group share inflight coalescing for concurrent + * lookups to the same hostname. + */ + public AddressResolverGroup getAddressResolverGroup() { + return addressResolverGroup; + } + public ClientStats getClientStats() { Map totalConnectionsPerHost = openChannels.stream() .map(Channel::remoteAddress) diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java index 08451ce953..8c988ecbf2 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java @@ -34,6 +34,7 @@ import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2StreamChannel; import io.netty.handler.codec.http2.Http2StreamChannelBootstrap; +import io.netty.resolver.AddressResolver; import io.netty.util.ReferenceCountUtil; import io.netty.util.Timer; import io.netty.util.concurrent.Future; @@ -46,6 +47,7 @@ import org.asynchttpclient.Realm; import org.asynchttpclient.Realm.AuthScheme; import org.asynchttpclient.Request; +import org.asynchttpclient.RequestBuilderBase; import org.asynchttpclient.exception.FilterException; import org.asynchttpclient.exception.PoolAlreadyClosedException; import org.asynchttpclient.exception.RemotelyClosedException; @@ -370,10 +372,18 @@ private Future> resolveAddresses(Request request, Pr Uri uri = request.getUri(); final Promise> promise = ImmediateEventExecutor.INSTANCE.newPromise(); + // Use the client's AddressResolverGroup when the request does not have a custom NameResolver. + // This provides non-blocking async DNS with inflight coalescing. + boolean useGroupResolver = request.getNameResolver() == RequestBuilderBase.DEFAULT_NAME_RESOLVER; + if (proxy != null && !proxy.isIgnoredForHost(uri.getHost()) && proxy.getProxyType().isHttp()) { int port = ProxyType.HTTPS.equals(proxy.getProxyType()) || uri.isSecured() ? proxy.getSecuredPort() : proxy.getPort(); InetSocketAddress unresolvedRemoteAddress = InetSocketAddress.createUnresolved(proxy.getHost(), port); scheduleRequestTimeout(future, unresolvedRemoteAddress); + if (useGroupResolver) { + AddressResolver resolver = channelManager.getAddressResolverGroup().getResolver(channelManager.getEventLoopGroup().next()); + return RequestHostnameResolver.INSTANCE.resolve(resolver, unresolvedRemoteAddress, asyncHandler); + } return RequestHostnameResolver.INSTANCE.resolve(request.getNameResolver(), unresolvedRemoteAddress, asyncHandler); } else { int port = uri.getExplicitPort(); @@ -385,6 +395,9 @@ private Future> resolveAddresses(Request request, Pr // bypass resolution InetSocketAddress inetSocketAddress = new InetSocketAddress(request.getAddress(), port); return promise.setSuccess(singletonList(inetSocketAddress)); + } else if (useGroupResolver) { + AddressResolver resolver = channelManager.getAddressResolverGroup().getResolver(channelManager.getEventLoopGroup().next()); + return RequestHostnameResolver.INSTANCE.resolve(resolver, unresolvedRemoteAddress, asyncHandler); } else { return RequestHostnameResolver.INSTANCE.resolve(request.getNameResolver(), unresolvedRemoteAddress, asyncHandler); } diff --git a/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java b/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java index b6330cf14a..32e6a7b626 100644 --- a/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java +++ b/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java @@ -15,6 +15,7 @@ */ package org.asynchttpclient.resolver; +import io.netty.resolver.AddressResolver; import io.netty.resolver.NameResolver; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.ImmediateEventExecutor; @@ -83,4 +84,58 @@ protected void onFailure(Throwable t) { return promise; } + + /** + * Resolve an unresolved address using an {@link AddressResolver} obtained from + * a {@link io.netty.resolver.AddressResolverGroup}. This path provides non-blocking + * DNS resolution with inflight coalescing. + * + * @param addressResolver the address resolver (from the group, bound to an event loop) + * @param unresolvedAddress the unresolved socket address + * @param asyncHandler the async handler for lifecycle callbacks + * @return a future that completes with the list of resolved addresses + */ + public Future> resolve(AddressResolver addressResolver, InetSocketAddress unresolvedAddress, AsyncHandler asyncHandler) { + final String hostname = unresolvedAddress.getHostString(); + final Promise> promise = ImmediateEventExecutor.INSTANCE.newPromise(); + + try { + asyncHandler.onHostnameResolutionAttempt(hostname); + } catch (Exception e) { + LOGGER.error("onHostnameResolutionAttempt crashed", e); + promise.tryFailure(e); + return promise; + } + + final Future> whenResolved = addressResolver.resolveAll(unresolvedAddress); + + whenResolved.addListener(new SimpleFutureListener>() { + + @Override + protected void onSuccess(List socketAddresses) { + try { + asyncHandler.onHostnameResolutionSuccess(hostname, socketAddresses); + } catch (Exception e) { + LOGGER.error("onHostnameResolutionSuccess crashed", e); + promise.tryFailure(e); + return; + } + promise.trySuccess(socketAddresses); + } + + @Override + protected void onFailure(Throwable t) { + try { + asyncHandler.onHostnameResolutionFailure(hostname, t); + } catch (Exception e) { + LOGGER.error("onHostnameResolutionFailure crashed", e); + promise.tryFailure(e); + return; + } + promise.tryFailure(t); + } + }); + + return promise; + } } From c612c4cf0b0c8544bef24d3061598d44d217b349 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:13:35 +0000 Subject: [PATCH 2/8] Fix native transport compatibility and UnknownHostException backward compat - TransportFactory now provides getDatagramChannelClass() for DNS resolver - Updated all transport factories (NIO, Epoll, KQueue, IoUring) with datagram channel support - ChannelManager uses transport-matched datagram channel for DnsAddressResolverGroup - RequestHostnameResolver wraps DNS errors in UnknownHostException for backward compat Agent-Logs-Url: https://github.com/doom369/async-http-client/sessions/b442c622-4c82-440d-84d0-50ee30bf3f27 Co-authored-by: doom369 <1536494+doom369@users.noreply.github.com> --- .../netty/channel/ChannelManager.java | 2 +- .../netty/channel/EpollTransportFactory.java | 7 +++++++ .../netty/channel/IoUringTransportFactory.java | 7 +++++++ .../netty/channel/KQueueTransportFactory.java | 7 +++++++ .../netty/channel/NioTransportFactory.java | 7 +++++++ .../netty/channel/TransportFactory.java | 7 +++++++ .../resolver/RequestHostnameResolver.java | 12 ++++++++++-- 7 files changed, 46 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java index 9547b0526e..739cf2a3e0 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java @@ -204,7 +204,7 @@ public ChannelManager(final AsyncHttpClientConfig config, Timer nettyTimer) { addressResolverGroup = config.getAddressResolverGroup(); allowCloseAddressResolverGroup = false; } else { - addressResolverGroup = new DnsAddressResolverGroup(io.netty.channel.socket.nio.NioDatagramChannel.class, + addressResolverGroup = new DnsAddressResolverGroup(transportFactory.getDatagramChannelClass(), io.netty.resolver.dns.DnsServerAddressStreamProviders.platformDefault()); allowCloseAddressResolverGroup = true; } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/EpollTransportFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/EpollTransportFactory.java index d24b32b706..aa07afcd52 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/EpollTransportFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/EpollTransportFactory.java @@ -16,8 +16,10 @@ package org.asynchttpclient.netty.channel; import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollDatagramChannel; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollSocketChannel; +import io.netty.channel.socket.DatagramChannel; import java.util.concurrent.ThreadFactory; @@ -41,4 +43,9 @@ public EpollSocketChannel newChannel() { public EpollEventLoopGroup newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { return new EpollEventLoopGroup(ioThreadsCount, threadFactory); } + + @Override + public Class getDatagramChannelClass() { + return EpollDatagramChannel.class; + } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/IoUringTransportFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/IoUringTransportFactory.java index a932501856..615cd0b68f 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/IoUringTransportFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/IoUringTransportFactory.java @@ -16,7 +16,9 @@ package org.asynchttpclient.netty.channel; import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.socket.DatagramChannel; import io.netty.channel.uring.IoUring; +import io.netty.channel.uring.IoUringDatagramChannel; import io.netty.channel.uring.IoUringIoHandler; import io.netty.channel.uring.IoUringSocketChannel; @@ -42,4 +44,9 @@ public IoUringSocketChannel newChannel() { public MultiThreadIoEventLoopGroup newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { return new MultiThreadIoEventLoopGroup(ioThreadsCount, threadFactory, IoUringIoHandler.newFactory()); } + + @Override + public Class getDatagramChannelClass() { + return IoUringDatagramChannel.class; + } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/KQueueTransportFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/KQueueTransportFactory.java index 54bcfe0d48..bb5b47df61 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/KQueueTransportFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/KQueueTransportFactory.java @@ -16,8 +16,10 @@ package org.asynchttpclient.netty.channel; import io.netty.channel.kqueue.KQueue; +import io.netty.channel.kqueue.KQueueDatagramChannel; import io.netty.channel.kqueue.KQueueEventLoopGroup; import io.netty.channel.kqueue.KQueueSocketChannel; +import io.netty.channel.socket.DatagramChannel; import java.util.concurrent.ThreadFactory; @@ -41,4 +43,9 @@ public KQueueSocketChannel newChannel() { public KQueueEventLoopGroup newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { return new KQueueEventLoopGroup(ioThreadsCount, threadFactory); } + + @Override + public Class getDatagramChannelClass() { + return KQueueDatagramChannel.class; + } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/NioTransportFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/NioTransportFactory.java index 96eeb37509..b0dd4b4883 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/NioTransportFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/NioTransportFactory.java @@ -16,6 +16,8 @@ package org.asynchttpclient.netty.channel; import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioSocketChannel; import java.util.concurrent.ThreadFactory; @@ -33,4 +35,9 @@ public NioSocketChannel newChannel() { public NioEventLoopGroup newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { return new NioEventLoopGroup(ioThreadsCount, threadFactory); } + + @Override + public Class getDatagramChannelClass() { + return NioDatagramChannel.class; + } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java index e833fdecf9..e4476849e0 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java @@ -18,10 +18,17 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelFactory; import io.netty.channel.EventLoopGroup; +import io.netty.channel.socket.DatagramChannel; import java.util.concurrent.ThreadFactory; public interface TransportFactory extends ChannelFactory { L newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory); + + /** + * Return the {@link DatagramChannel} class suitable for this transport. + * Used by DNS resolution to create UDP channels compatible with the event loop group. + */ + Class getDatagramChannelClass(); } diff --git a/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java b/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java index 32e6a7b626..5fa16436db 100644 --- a/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java +++ b/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java @@ -27,6 +27,7 @@ import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; @@ -125,14 +126,21 @@ protected void onSuccess(List socketAddresses) { @Override protected void onFailure(Throwable t) { + // Wrap non-UnknownHostException DNS errors for backward compatibility. + // The JDK resolver always threw UnknownHostException; Netty's async DNS + // resolver may throw DnsErrorCauseException for SERVFAIL or other errors. + Throwable cause = t instanceof UnknownHostException ? t : new UnknownHostException(hostname); + if (!(t instanceof UnknownHostException)) { + cause.initCause(t); + } try { - asyncHandler.onHostnameResolutionFailure(hostname, t); + asyncHandler.onHostnameResolutionFailure(hostname, cause); } catch (Exception e) { LOGGER.error("onHostnameResolutionFailure crashed", e); promise.tryFailure(e); return; } - promise.tryFailure(t); + promise.tryFailure(cause); } }); From d7ac9fa71fbdd14b1a5ac92c87722c8a3b8f1cfa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:24:26 +0000 Subject: [PATCH 3/8] Don't override default behavior: use config property to opt-in to async DNS - Remove deprecation from DEFAULT_NAME_RESOLVER (restored to original) - ChannelManager no longer auto-creates DnsAddressResolverGroup; uses config value or null - NettyRequestSender checks config.getAddressResolverGroup() instead of identity check - getBootstrap falls back to per-request NameResolver when no group is configured - Default behavior is unchanged; users opt-in via setAddressResolverGroup() Agent-Logs-Url: https://github.com/doom369/async-http-client/sessions/f6e6623f-4edb-4e85-92bf-2b1f44f3c066 Co-authored-by: doom369 <1536494+doom369@users.noreply.github.com> --- .../asynchttpclient/RequestBuilderBase.java | 9 -- .../netty/channel/ChannelManager.java | 111 +++++++++--------- .../netty/request/NettyRequestSender.java | 18 ++- 3 files changed, 66 insertions(+), 72 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java b/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java index eee9c9cecf..29bbaa670e 100644 --- a/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java +++ b/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java @@ -60,15 +60,6 @@ public abstract class RequestBuilderBase> { private static final Logger LOGGER = LoggerFactory.getLogger(RequestBuilderBase.class); private static final Uri DEFAULT_REQUEST_URL = Uri.create("http://localhost"); - /** - * @deprecated This blocking resolver is retained only as a sentinel value for backward compatibility. - * When a request uses this default, the client's {@link io.netty.resolver.AddressResolverGroup} - * (typically {@link io.netty.resolver.dns.DnsAddressResolverGroup}) is used instead, providing - * non-blocking DNS with inflight coalescing. To customize DNS resolution, either configure an - * {@link io.netty.resolver.AddressResolverGroup} on the client config or set a custom - * {@link NameResolver} on individual requests. - */ - @Deprecated public static final NameResolver DEFAULT_NAME_RESOLVER = new DefaultNameResolver(ImmediateEventExecutor.INSTANCE); // builder only fields protected UriEncoder uriEncoder; diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java index 739cf2a3e0..9f3d309058 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java @@ -56,7 +56,6 @@ import io.netty.resolver.AddressResolver; import io.netty.resolver.AddressResolverGroup; import io.netty.resolver.NameResolver; -import io.netty.resolver.dns.DnsAddressResolverGroup; import io.netty.util.Timer; import io.netty.util.concurrent.DefaultThreadFactory; import io.netty.util.concurrent.Future; @@ -85,6 +84,7 @@ import org.asynchttpclient.proxy.ProxyServer; import org.asynchttpclient.proxy.ProxyType; import org.asynchttpclient.uri.Uri; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -125,8 +125,7 @@ public class ChannelManager { private final Bootstrap httpBootstrap; private final Bootstrap wsBootstrap; private final long handshakeTimeout; - private final AddressResolverGroup addressResolverGroup; - private final boolean allowCloseAddressResolverGroup; + private final @Nullable AddressResolverGroup addressResolverGroup; private final ChannelPool channelPool; private final ChannelGroup openChannels; @@ -199,15 +198,8 @@ public ChannelManager(final AsyncHttpClientConfig config, Timer nettyTimer) { httpBootstrap = newBootstrap(transportFactory, eventLoopGroup, config); wsBootstrap = newBootstrap(transportFactory, eventLoopGroup, config); - // Initialize the address resolver group for async DNS - if (config.getAddressResolverGroup() != null) { - addressResolverGroup = config.getAddressResolverGroup(); - allowCloseAddressResolverGroup = false; - } else { - addressResolverGroup = new DnsAddressResolverGroup(transportFactory.getDatagramChannelClass(), - io.netty.resolver.dns.DnsServerAddressStreamProviders.platformDefault()); - allowCloseAddressResolverGroup = true; - } + // Use the address resolver group from config if provided; otherwise null (legacy per-request resolution) + addressResolverGroup = config.getAddressResolverGroup(); } private static TransportFactory getNativeTransportFactory(AsyncHttpClientConfig config) { @@ -424,7 +416,7 @@ private void doClose() { ChannelGroupFuture groupFuture = openChannels.close(); channelPool.destroy(); groupFuture.addListener(future -> sslEngineFactory.destroy()); - if (allowCloseAddressResolverGroup) { + if (addressResolverGroup != null) { addressResolverGroup.close(); } } @@ -597,42 +589,27 @@ public Future getBootstrap(Uri uri, NameResolver nameRes Bootstrap socksBootstrap = httpBootstrap.clone(); ChannelHandler httpBootstrapHandler = socksBootstrap.config().handler(); - // Use the address resolver group for async, non-blocking proxy host resolution - InetSocketAddress unresolvedProxyAddress = InetSocketAddress.createUnresolved(proxy.getHost(), proxy.getPort()); - AddressResolver resolver = addressResolverGroup.getResolver(eventLoopGroup.next()); - resolver.resolve(unresolvedProxyAddress).addListener((Future whenProxyAddress) -> { - if (whenProxyAddress.isSuccess()) { - socksBootstrap.handler(new ChannelInitializer() { - @Override - protected void initChannel(Channel channel) throws Exception { - channel.pipeline().addLast(httpBootstrapHandler); - - InetSocketAddress proxyAddress = whenProxyAddress.get(); - Realm realm = proxy.getRealm(); - String username = realm != null ? realm.getPrincipal() : null; - String password = realm != null ? realm.getPassword() : null; - ProxyHandler socksProxyHandler; - switch (proxy.getProxyType()) { - case SOCKS_V4: - socksProxyHandler = new Socks4ProxyHandler(proxyAddress, username); - break; - - case SOCKS_V5: - socksProxyHandler = new Socks5ProxyHandler(proxyAddress, username, password); - break; - - default: - throw new IllegalArgumentException("Only SOCKS4 and SOCKS5 supported at the moment."); - } - channel.pipeline().addFirst(SOCKS_HANDLER, socksProxyHandler); - } - }); - promise.setSuccess(socksBootstrap); - - } else { - promise.setFailure(whenProxyAddress.cause()); - } - }); + if (addressResolverGroup != null) { + // Use the address resolver group for async, non-blocking proxy host resolution + InetSocketAddress unresolvedProxyAddress = InetSocketAddress.createUnresolved(proxy.getHost(), proxy.getPort()); + AddressResolver resolver = addressResolverGroup.getResolver(eventLoopGroup.next()); + resolver.resolve(unresolvedProxyAddress).addListener((Future whenProxyAddress) -> { + if (whenProxyAddress.isSuccess()) { + configureSocksBootstrap(socksBootstrap, httpBootstrapHandler, whenProxyAddress.get(), proxy, promise); + } else { + promise.setFailure(whenProxyAddress.cause()); + } + }); + } else { + nameResolver.resolve(proxy.getHost()).addListener((Future whenProxyAddress) -> { + if (whenProxyAddress.isSuccess()) { + InetSocketAddress proxyAddress = new InetSocketAddress(whenProxyAddress.get(), proxy.getPort()); + configureSocksBootstrap(socksBootstrap, httpBootstrapHandler, proxyAddress, proxy, promise); + } else { + promise.setFailure(whenProxyAddress.cause()); + } + }); + } } else if (proxy != null && ProxyType.HTTPS.equals(proxy.getProxyType())) { // For HTTPS proxies, use HTTP bootstrap but ensure SSL connection to proxy @@ -645,6 +622,35 @@ protected void initChannel(Channel channel) throws Exception { return promise; } + private void configureSocksBootstrap(Bootstrap socksBootstrap, ChannelHandler httpBootstrapHandler, + InetSocketAddress proxyAddress, ProxyServer proxy, Promise promise) { + socksBootstrap.handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel channel) throws Exception { + channel.pipeline().addLast(httpBootstrapHandler); + + Realm realm = proxy.getRealm(); + String username = realm != null ? realm.getPrincipal() : null; + String password = realm != null ? realm.getPassword() : null; + ProxyHandler socksProxyHandler; + switch (proxy.getProxyType()) { + case SOCKS_V4: + socksProxyHandler = new Socks4ProxyHandler(proxyAddress, username); + break; + + case SOCKS_V5: + socksProxyHandler = new Socks5ProxyHandler(proxyAddress, username, password); + break; + + default: + throw new IllegalArgumentException("Only SOCKS4 and SOCKS5 supported at the moment."); + } + channel.pipeline().addFirst(SOCKS_HANDLER, socksProxyHandler); + } + }); + promise.setSuccess(socksBootstrap); + } + /** * Checks whether the given channel is an HTTP/2 connection (i.e. has the HTTP/2 multiplex handler installed). */ @@ -812,11 +818,10 @@ public EventLoopGroup getEventLoopGroup() { } /** - * Return the {@link AddressResolverGroup} used for async DNS resolution. - * Resolvers obtained from this group share inflight coalescing for concurrent - * lookups to the same hostname. + * Return the {@link AddressResolverGroup} used for async DNS resolution, or {@code null} + * if per-request name resolvers should be used (legacy behavior). */ - public AddressResolverGroup getAddressResolverGroup() { + public @Nullable AddressResolverGroup getAddressResolverGroup() { return addressResolverGroup; } diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java index 8c988ecbf2..059764f41d 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java @@ -47,7 +47,6 @@ import org.asynchttpclient.Realm; import org.asynchttpclient.Realm.AuthScheme; import org.asynchttpclient.Request; -import org.asynchttpclient.RequestBuilderBase; import org.asynchttpclient.exception.FilterException; import org.asynchttpclient.exception.PoolAlreadyClosedException; import org.asynchttpclient.exception.RemotelyClosedException; @@ -372,17 +371,17 @@ private Future> resolveAddresses(Request request, Pr Uri uri = request.getUri(); final Promise> promise = ImmediateEventExecutor.INSTANCE.newPromise(); - // Use the client's AddressResolverGroup when the request does not have a custom NameResolver. - // This provides non-blocking async DNS with inflight coalescing. - boolean useGroupResolver = request.getNameResolver() == RequestBuilderBase.DEFAULT_NAME_RESOLVER; + // Use the client's AddressResolverGroup when configured for non-blocking async DNS. + AddressResolver groupResolver = channelManager.getAddressResolverGroup() != null + ? channelManager.getAddressResolverGroup().getResolver(channelManager.getEventLoopGroup().next()) + : null; if (proxy != null && !proxy.isIgnoredForHost(uri.getHost()) && proxy.getProxyType().isHttp()) { int port = ProxyType.HTTPS.equals(proxy.getProxyType()) || uri.isSecured() ? proxy.getSecuredPort() : proxy.getPort(); InetSocketAddress unresolvedRemoteAddress = InetSocketAddress.createUnresolved(proxy.getHost(), port); scheduleRequestTimeout(future, unresolvedRemoteAddress); - if (useGroupResolver) { - AddressResolver resolver = channelManager.getAddressResolverGroup().getResolver(channelManager.getEventLoopGroup().next()); - return RequestHostnameResolver.INSTANCE.resolve(resolver, unresolvedRemoteAddress, asyncHandler); + if (groupResolver != null) { + return RequestHostnameResolver.INSTANCE.resolve(groupResolver, unresolvedRemoteAddress, asyncHandler); } return RequestHostnameResolver.INSTANCE.resolve(request.getNameResolver(), unresolvedRemoteAddress, asyncHandler); } else { @@ -395,9 +394,8 @@ private Future> resolveAddresses(Request request, Pr // bypass resolution InetSocketAddress inetSocketAddress = new InetSocketAddress(request.getAddress(), port); return promise.setSuccess(singletonList(inetSocketAddress)); - } else if (useGroupResolver) { - AddressResolver resolver = channelManager.getAddressResolverGroup().getResolver(channelManager.getEventLoopGroup().next()); - return RequestHostnameResolver.INSTANCE.resolve(resolver, unresolvedRemoteAddress, asyncHandler); + } else if (groupResolver != null) { + return RequestHostnameResolver.INSTANCE.resolve(groupResolver, unresolvedRemoteAddress, asyncHandler); } else { return RequestHostnameResolver.INSTANCE.resolve(request.getNameResolver(), unresolvedRemoteAddress, asyncHandler); } From 2153b525377c34b4522596989532f8a6ff4f902c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:15:12 +0000 Subject: [PATCH 4/8] Remove getDatagramChannelClass() from TransportFactory, add AddressResolverGroup test - Revert TransportFactory and all 4 implementations to original state (remove getDatagramChannelClass() which is unnecessary since resolver group is user-provided) - Add AddressResolverGroupTest with 4 test cases covering opt-in async DNS - Fix javadoc to reflect opt-in (not auto-created) behavior Agent-Logs-Url: https://github.com/doom369/async-http-client/sessions/dc06458d-dac1-45b3-8e11-c62caeaf3a3d Co-authored-by: doom369 <1536494+doom369@users.noreply.github.com> --- .../AsyncHttpClientConfig.java | 9 +- .../DefaultAsyncHttpClientConfig.java | 5 +- .../netty/channel/EpollTransportFactory.java | 7 - .../channel/IoUringTransportFactory.java | 7 - .../netty/channel/KQueueTransportFactory.java | 7 - .../netty/channel/NioTransportFactory.java | 7 - .../netty/channel/TransportFactory.java | 7 - .../AddressResolverGroupTest.java | 128 ++++++++++++++++++ 8 files changed, 134 insertions(+), 43 deletions(-) create mode 100644 client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java diff --git a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java index b84413acde..5f373ccc2a 100644 --- a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java @@ -381,13 +381,14 @@ default boolean isHttp2CleartextEnabled() { * Return the {@link AddressResolverGroup} used for asynchronous DNS resolution. *

* When non-null, this resolver group is used for hostname resolution instead of - * the per-request {@link io.netty.resolver.NameResolver}. The default implementation - * uses Netty's {@link io.netty.resolver.dns.DnsAddressResolverGroup} which provides + * the per-request {@link io.netty.resolver.NameResolver}. For example, + * Netty's {@link io.netty.resolver.dns.DnsAddressResolverGroup} provides * non-blocking DNS lookups with inflight coalescing across concurrent requests for * the same hostname. *

- * Providing {@code null} disables the group resolver and falls back to - * per-request name resolvers (legacy behavior). + * By default this returns {@code null}, preserving the legacy per-request name + * resolver behavior. Set via + * {@link DefaultAsyncHttpClientConfig.Builder#setAddressResolverGroup(AddressResolverGroup)}. * * @return the {@link AddressResolverGroup} or {@code null} to use per-request resolvers */ diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java index 56f0c0e28d..7e2bb17aa5 100644 --- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java @@ -1530,10 +1530,7 @@ public Builder setEventLoopGroup(EventLoopGroup eventLoopGroup) { * Set a custom {@link AddressResolverGroup} for asynchronous DNS resolution. *

* When set, this resolver group is used instead of the per-request {@link io.netty.resolver.NameResolver}. - * Pass {@code null} to disable the group resolver and fall back to per-request resolvers (legacy behavior). - *

- * If not explicitly set, a {@link io.netty.resolver.dns.DnsAddressResolverGroup} is created automatically, - * providing non-blocking DNS with inflight coalescing. + * Pass {@code null} (the default) to use per-request resolvers (legacy behavior). * * @param addressResolverGroup the resolver group, or {@code null} to use per-request resolvers * @return the same builder instance diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/EpollTransportFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/EpollTransportFactory.java index aa07afcd52..d24b32b706 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/EpollTransportFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/EpollTransportFactory.java @@ -16,10 +16,8 @@ package org.asynchttpclient.netty.channel; import io.netty.channel.epoll.Epoll; -import io.netty.channel.epoll.EpollDatagramChannel; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollSocketChannel; -import io.netty.channel.socket.DatagramChannel; import java.util.concurrent.ThreadFactory; @@ -43,9 +41,4 @@ public EpollSocketChannel newChannel() { public EpollEventLoopGroup newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { return new EpollEventLoopGroup(ioThreadsCount, threadFactory); } - - @Override - public Class getDatagramChannelClass() { - return EpollDatagramChannel.class; - } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/IoUringTransportFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/IoUringTransportFactory.java index 615cd0b68f..a932501856 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/IoUringTransportFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/IoUringTransportFactory.java @@ -16,9 +16,7 @@ package org.asynchttpclient.netty.channel; import io.netty.channel.MultiThreadIoEventLoopGroup; -import io.netty.channel.socket.DatagramChannel; import io.netty.channel.uring.IoUring; -import io.netty.channel.uring.IoUringDatagramChannel; import io.netty.channel.uring.IoUringIoHandler; import io.netty.channel.uring.IoUringSocketChannel; @@ -44,9 +42,4 @@ public IoUringSocketChannel newChannel() { public MultiThreadIoEventLoopGroup newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { return new MultiThreadIoEventLoopGroup(ioThreadsCount, threadFactory, IoUringIoHandler.newFactory()); } - - @Override - public Class getDatagramChannelClass() { - return IoUringDatagramChannel.class; - } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/KQueueTransportFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/KQueueTransportFactory.java index bb5b47df61..54bcfe0d48 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/KQueueTransportFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/KQueueTransportFactory.java @@ -16,10 +16,8 @@ package org.asynchttpclient.netty.channel; import io.netty.channel.kqueue.KQueue; -import io.netty.channel.kqueue.KQueueDatagramChannel; import io.netty.channel.kqueue.KQueueEventLoopGroup; import io.netty.channel.kqueue.KQueueSocketChannel; -import io.netty.channel.socket.DatagramChannel; import java.util.concurrent.ThreadFactory; @@ -43,9 +41,4 @@ public KQueueSocketChannel newChannel() { public KQueueEventLoopGroup newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { return new KQueueEventLoopGroup(ioThreadsCount, threadFactory); } - - @Override - public Class getDatagramChannelClass() { - return KQueueDatagramChannel.class; - } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/NioTransportFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/NioTransportFactory.java index b0dd4b4883..96eeb37509 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/NioTransportFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/NioTransportFactory.java @@ -16,8 +16,6 @@ package org.asynchttpclient.netty.channel; import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.DatagramChannel; -import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioSocketChannel; import java.util.concurrent.ThreadFactory; @@ -35,9 +33,4 @@ public NioSocketChannel newChannel() { public NioEventLoopGroup newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { return new NioEventLoopGroup(ioThreadsCount, threadFactory); } - - @Override - public Class getDatagramChannelClass() { - return NioDatagramChannel.class; - } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java index e4476849e0..e833fdecf9 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java @@ -18,17 +18,10 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelFactory; import io.netty.channel.EventLoopGroup; -import io.netty.channel.socket.DatagramChannel; import java.util.concurrent.ThreadFactory; public interface TransportFactory extends ChannelFactory { L newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory); - - /** - * Return the {@link DatagramChannel} class suitable for this transport. - * Used by DNS resolution to create UDP channels compatible with the event loop group. - */ - Class getDatagramChannelClass(); } diff --git a/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java b/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java new file mode 100644 index 0000000000..13bcb4ad33 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2024 AsyncHttpClient Project. All rights reserved. + * + * Licensed 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. + */ +package org.asynchttpclient; + +import io.github.artsok.RepeatedIfExceptionsTest; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.resolver.dns.DnsAddressResolverGroup; +import io.netty.resolver.dns.DnsServerAddressStreamProviders; +import org.asynchttpclient.test.EventCollectingHandler; +import org.asynchttpclient.testserver.HttpServer; +import org.asynchttpclient.testserver.HttpTest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import java.util.Arrays; +import java.util.concurrent.ExecutionException; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.asynchttpclient.Dsl.config; +import static org.asynchttpclient.Dsl.get; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class AddressResolverGroupTest extends HttpTest { + + private HttpServer server; + + @BeforeEach + public void start() throws Throwable { + server = new HttpServer(); + server.start(); + } + + @AfterEach + public void stop() throws Throwable { + server.close(); + } + + private String getTargetUrl() { + return server.getHttpUrl() + "/foo/bar"; + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void requestWithDnsAddressResolverGroupSucceeds() throws Throwable { + DnsAddressResolverGroup resolverGroup = new DnsAddressResolverGroup( + NioDatagramChannel.class, + DnsServerAddressStreamProviders.platformDefault()); + + withClient(config().setAddressResolverGroup(resolverGroup)).run(client -> + withServer(server).run(server -> { + server.enqueueOk(); + Response response = client.prepareGet(getTargetUrl()).execute().get(3, SECONDS); + assertEquals(200, response.getStatusCode()); + })); + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void dnsResolverGroupFiresHostnameResolutionEvents() throws Throwable { + DnsAddressResolverGroup resolverGroup = new DnsAddressResolverGroup( + NioDatagramChannel.class, + DnsServerAddressStreamProviders.platformDefault()); + + withClient(config().setAddressResolverGroup(resolverGroup)).run(client -> + withServer(server).run(server -> { + server.enqueueOk(); + Request request = get(getTargetUrl()).build(); + EventCollectingHandler handler = new EventCollectingHandler(); + client.executeRequest(request, handler).get(3, SECONDS); + handler.waitForCompletion(3, SECONDS); + + Object[] expectedEvents = { + CONNECTION_POOL_EVENT, + HOSTNAME_RESOLUTION_EVENT, + HOSTNAME_RESOLUTION_SUCCESS_EVENT, + CONNECTION_OPEN_EVENT, + CONNECTION_SUCCESS_EVENT, + REQUEST_SEND_EVENT, + HEADERS_WRITTEN_EVENT, + STATUS_RECEIVED_EVENT, + HEADERS_RECEIVED_EVENT, + CONNECTION_OFFER_EVENT, + COMPLETED_EVENT}; + + assertArrayEquals(expectedEvents, handler.firedEvents.toArray(), + "Got " + Arrays.toString(handler.firedEvents.toArray())); + })); + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void defaultConfigDoesNotSetAddressResolverGroup() { + DefaultAsyncHttpClientConfig config = config().build(); + assertNull(config.getAddressResolverGroup(), + "Default config should not have an AddressResolverGroup"); + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void unknownHostWithDnsResolverGroupFails() throws Throwable { + DnsAddressResolverGroup resolverGroup = new DnsAddressResolverGroup( + NioDatagramChannel.class, + DnsServerAddressStreamProviders.platformDefault()); + + withClient(config().setAddressResolverGroup(resolverGroup)).run(client -> { + try { + client.prepareGet("http://nonexistent.invalid/foo").execute().get(10, SECONDS); + } catch (ExecutionException e) { + // DNS failures may surface as UnknownHostException or as a Netty + // DnsErrorCauseException (since MiscUtils.getCause unwraps the chain). + // Either way, the request must fail, never silently succeed. + assertNotNull(e.getCause(), "Should have a cause for the DNS failure"); + } + }); + } +} From 0973e8c5d2119aa4e3d44d6d3284d16fb51f2271 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:39:45 +0000 Subject: [PATCH 5/8] Add real domain tests, make getAddressResolverGroup() default method, lazy resolver init - Added tests for real domains (google.com, example.com) with network availability check via assumeTrue - Made getAddressResolverGroup() a default method returning null in the interface to avoid breaking downstream implementations - Lazily obtain group resolver only in branches that need DNS resolution - Added fail() to unknownHost test for proper assertion - Documented resolver group lifecycle ownership in builder Javadoc Agent-Logs-Url: https://github.com/doom369/async-http-client/sessions/ef98f786-3665-4a5f-9005-60ebdba761b1 Co-authored-by: doom369 <1536494+doom369@users.noreply.github.com> --- .../AsyncHttpClientConfig.java | 4 +- .../DefaultAsyncHttpClientConfig.java | 5 ++ .../netty/request/NettyRequestSender.java | 17 ++++-- .../AddressResolverGroupTest.java | 57 ++++++++++++++++++- 4 files changed, 73 insertions(+), 10 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java index 5f373ccc2a..6bdb2248d3 100644 --- a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java @@ -393,7 +393,9 @@ default boolean isHttp2CleartextEnabled() { * @return the {@link AddressResolverGroup} or {@code null} to use per-request resolvers */ @Nullable - AddressResolverGroup getAddressResolverGroup(); + default AddressResolverGroup getAddressResolverGroup() { + return null; + } boolean isUseNativeTransport(); diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java index 7e2bb17aa5..74f9937b54 100644 --- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java @@ -1531,6 +1531,11 @@ public Builder setEventLoopGroup(EventLoopGroup eventLoopGroup) { *

* When set, this resolver group is used instead of the per-request {@link io.netty.resolver.NameResolver}. * Pass {@code null} (the default) to use per-request resolvers (legacy behavior). + *

+ * Lifecycle: The client takes ownership of the provided resolver group and will + * {@linkplain AddressResolverGroup#close() close} it when the client is shut down. + * Do not pass a shared resolver group that is used by other clients unless you manage + * its lifecycle independently. * * @param addressResolverGroup the resolver group, or {@code null} to use per-request resolvers * @return the same builder instance diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java index 059764f41d..2197d8c709 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java @@ -35,6 +35,7 @@ import io.netty.handler.codec.http2.Http2StreamChannel; import io.netty.handler.codec.http2.Http2StreamChannelBootstrap; import io.netty.resolver.AddressResolver; +import io.netty.resolver.AddressResolverGroup; import io.netty.util.ReferenceCountUtil; import io.netty.util.Timer; import io.netty.util.concurrent.Future; @@ -73,6 +74,7 @@ import org.asynchttpclient.resolver.RequestHostnameResolver; import org.asynchttpclient.uri.Uri; import org.asynchttpclient.ws.WebSocketUpgradeHandler; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -371,15 +373,11 @@ private Future> resolveAddresses(Request request, Pr Uri uri = request.getUri(); final Promise> promise = ImmediateEventExecutor.INSTANCE.newPromise(); - // Use the client's AddressResolverGroup when configured for non-blocking async DNS. - AddressResolver groupResolver = channelManager.getAddressResolverGroup() != null - ? channelManager.getAddressResolverGroup().getResolver(channelManager.getEventLoopGroup().next()) - : null; - if (proxy != null && !proxy.isIgnoredForHost(uri.getHost()) && proxy.getProxyType().isHttp()) { int port = ProxyType.HTTPS.equals(proxy.getProxyType()) || uri.isSecured() ? proxy.getSecuredPort() : proxy.getPort(); InetSocketAddress unresolvedRemoteAddress = InetSocketAddress.createUnresolved(proxy.getHost(), port); scheduleRequestTimeout(future, unresolvedRemoteAddress); + AddressResolver groupResolver = getGroupResolver(); if (groupResolver != null) { return RequestHostnameResolver.INSTANCE.resolve(groupResolver, unresolvedRemoteAddress, asyncHandler); } @@ -394,7 +392,9 @@ private Future> resolveAddresses(Request request, Pr // bypass resolution InetSocketAddress inetSocketAddress = new InetSocketAddress(request.getAddress(), port); return promise.setSuccess(singletonList(inetSocketAddress)); - } else if (groupResolver != null) { + } + AddressResolver groupResolver = getGroupResolver(); + if (groupResolver != null) { return RequestHostnameResolver.INSTANCE.resolve(groupResolver, unresolvedRemoteAddress, asyncHandler); } else { return RequestHostnameResolver.INSTANCE.resolve(request.getNameResolver(), unresolvedRemoteAddress, asyncHandler); @@ -402,6 +402,11 @@ private Future> resolveAddresses(Request request, Pr } } + private @Nullable AddressResolver getGroupResolver() { + AddressResolverGroup group = channelManager.getAddressResolverGroup(); + return group != null ? group.getResolver(channelManager.getEventLoopGroup().next()) : null; + } + private NettyResponseFuture newNettyResponseFuture(Request request, AsyncHandler asyncHandler, NettyRequest nettyRequest, ProxyServer proxyServer) { NettyResponseFuture future = new NettyResponseFuture<>( request, diff --git a/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java b/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java index 13bcb4ad33..72cf5c804b 100644 --- a/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java +++ b/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java @@ -25,21 +25,37 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import java.net.InetSocketAddress; +import java.net.Socket; import java.util.Arrays; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import static java.util.concurrent.TimeUnit.SECONDS; +import static org.asynchttpclient.Dsl.asyncHttpClient; import static org.asynchttpclient.Dsl.config; import static org.asynchttpclient.Dsl.get; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; public class AddressResolverGroupTest extends HttpTest { private HttpServer server; + private static boolean isExternalNetworkAvailable() { + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress("www.google.com", 443), 3000); + return true; + } catch (Exception e) { + return false; + } + } + @BeforeEach public void start() throws Throwable { server = new HttpServer(); @@ -117,12 +133,47 @@ public void unknownHostWithDnsResolverGroupFails() throws Throwable { withClient(config().setAddressResolverGroup(resolverGroup)).run(client -> { try { client.prepareGet("http://nonexistent.invalid/foo").execute().get(10, SECONDS); + fail("Request to nonexistent host should have thrown an exception"); } catch (ExecutionException e) { - // DNS failures may surface as UnknownHostException or as a Netty - // DnsErrorCauseException (since MiscUtils.getCause unwraps the chain). - // Either way, the request must fail, never silently succeed. assertNotNull(e.getCause(), "Should have a cause for the DNS failure"); } }); } + + @RepeatedIfExceptionsTest(repeats = 5) + public void resolveRealDomainWithDnsResolverGroup() throws Throwable { + assumeTrue(isExternalNetworkAvailable(), "External network not available - skipping test"); + + DnsAddressResolverGroup resolverGroup = new DnsAddressResolverGroup( + NioDatagramChannel.class, + DnsServerAddressStreamProviders.platformDefault()); + + try (AsyncHttpClient client = asyncHttpClient(config().setAddressResolverGroup(resolverGroup))) { + Response response = client.prepareGet("https://www.google.com/").execute().get(20, TimeUnit.SECONDS); + assertNotNull(response); + assertTrue(response.getStatusCode() >= 200 && response.getStatusCode() < 400, + "Expected successful HTTP status but got " + response.getStatusCode()); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void resolveMultipleRealDomainsWithDnsResolverGroup() throws Throwable { + assumeTrue(isExternalNetworkAvailable(), "External network not available - skipping test"); + + DnsAddressResolverGroup resolverGroup = new DnsAddressResolverGroup( + NioDatagramChannel.class, + DnsServerAddressStreamProviders.platformDefault()); + + try (AsyncHttpClient client = asyncHttpClient(config().setAddressResolverGroup(resolverGroup))) { + Response response1 = client.prepareGet("https://www.google.com/").execute().get(20, TimeUnit.SECONDS); + assertNotNull(response1); + assertTrue(response1.getStatusCode() >= 200 && response1.getStatusCode() < 400, + "Expected successful HTTP status for google.com but got " + response1.getStatusCode()); + + Response response2 = client.prepareGet("https://www.example.com/").execute().get(20, TimeUnit.SECONDS); + assertNotNull(response2); + assertTrue(response2.getStatusCode() >= 200 && response2.getStatusCode() < 400, + "Expected successful HTTP status for example.com but got " + response2.getStatusCode()); + } + } } From 9f4468262738a18ec188f4a6adbe22a7cf9f376f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:41:51 +0000 Subject: [PATCH 6/8] Extract real domain URLs as constants in test class Agent-Logs-Url: https://github.com/doom369/async-http-client/sessions/ef98f786-3665-4a5f-9005-60ebdba761b1 Co-authored-by: doom369 <1536494+doom369@users.noreply.github.com> --- .../org/asynchttpclient/AddressResolverGroupTest.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java b/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java index 72cf5c804b..5bd85eaccf 100644 --- a/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java +++ b/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java @@ -45,6 +45,9 @@ public class AddressResolverGroupTest extends HttpTest { + private static final String GOOGLE_URL = "https://www.google.com/"; + private static final String EXAMPLE_URL = "https://www.example.com/"; + private HttpServer server; private static boolean isExternalNetworkAvailable() { @@ -149,7 +152,7 @@ public void resolveRealDomainWithDnsResolverGroup() throws Throwable { DnsServerAddressStreamProviders.platformDefault()); try (AsyncHttpClient client = asyncHttpClient(config().setAddressResolverGroup(resolverGroup))) { - Response response = client.prepareGet("https://www.google.com/").execute().get(20, TimeUnit.SECONDS); + Response response = client.prepareGet(GOOGLE_URL).execute().get(20, TimeUnit.SECONDS); assertNotNull(response); assertTrue(response.getStatusCode() >= 200 && response.getStatusCode() < 400, "Expected successful HTTP status but got " + response.getStatusCode()); @@ -165,12 +168,12 @@ public void resolveMultipleRealDomainsWithDnsResolverGroup() throws Throwable { DnsServerAddressStreamProviders.platformDefault()); try (AsyncHttpClient client = asyncHttpClient(config().setAddressResolverGroup(resolverGroup))) { - Response response1 = client.prepareGet("https://www.google.com/").execute().get(20, TimeUnit.SECONDS); + Response response1 = client.prepareGet(GOOGLE_URL).execute().get(20, TimeUnit.SECONDS); assertNotNull(response1); assertTrue(response1.getStatusCode() >= 200 && response1.getStatusCode() < 400, "Expected successful HTTP status for google.com but got " + response1.getStatusCode()); - Response response2 = client.prepareGet("https://www.example.com/").execute().get(20, TimeUnit.SECONDS); + Response response2 = client.prepareGet(EXAMPLE_URL).execute().get(20, TimeUnit.SECONDS); assertNotNull(response2); assertTrue(response2.getStatusCode() >= 200 && response2.getStatusCode() < 400, "Expected successful HTTP status for example.com but got " + response2.getStatusCode()); From 361053da0b3951aa4ae12bb3a3e8440e3e466b1d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 05:07:41 +0000 Subject: [PATCH 7/8] Extract resolveHostname helper, close resolver group before EventLoopGroup, simplify error handling Agent-Logs-Url: https://github.com/doom369/async-http-client/sessions/b617a76d-89c2-45ca-b160-407a5737bcc7 Co-authored-by: doom369 <1536494+doom369@users.noreply.github.com> --- .../netty/channel/ChannelManager.java | 8 ++++--- .../netty/request/NettyRequestSender.java | 23 ++++++++----------- .../resolver/RequestHostnameResolver.java | 13 +++-------- 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java index 9f3d309058..3db64e64ad 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java @@ -416,12 +416,14 @@ private void doClose() { ChannelGroupFuture groupFuture = openChannels.close(); channelPool.destroy(); groupFuture.addListener(future -> sslEngineFactory.destroy()); - if (addressResolverGroup != null) { - addressResolverGroup.close(); - } } public void close() { + // Close the resolver group first while the EventLoopGroup is still active, + // since Netty DNS resolvers may need a live EventLoop for clean shutdown. + if (addressResolverGroup != null) { + addressResolverGroup.close(); + } if (allowReleaseEventLoopGroup) { final long shutdownQuietPeriod = config.getShutdownQuietPeriod().toMillis(); final long shutdownTimeout = config.getShutdownTimeout().toMillis(); diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java index 2197d8c709..73679fd17e 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java @@ -74,7 +74,7 @@ import org.asynchttpclient.resolver.RequestHostnameResolver; import org.asynchttpclient.uri.Uri; import org.asynchttpclient.ws.WebSocketUpgradeHandler; -import org.jetbrains.annotations.Nullable; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -377,11 +377,7 @@ private Future> resolveAddresses(Request request, Pr int port = ProxyType.HTTPS.equals(proxy.getProxyType()) || uri.isSecured() ? proxy.getSecuredPort() : proxy.getPort(); InetSocketAddress unresolvedRemoteAddress = InetSocketAddress.createUnresolved(proxy.getHost(), port); scheduleRequestTimeout(future, unresolvedRemoteAddress); - AddressResolver groupResolver = getGroupResolver(); - if (groupResolver != null) { - return RequestHostnameResolver.INSTANCE.resolve(groupResolver, unresolvedRemoteAddress, asyncHandler); - } - return RequestHostnameResolver.INSTANCE.resolve(request.getNameResolver(), unresolvedRemoteAddress, asyncHandler); + return resolveHostname(request, unresolvedRemoteAddress, asyncHandler); } else { int port = uri.getExplicitPort(); @@ -393,18 +389,17 @@ private Future> resolveAddresses(Request request, Pr InetSocketAddress inetSocketAddress = new InetSocketAddress(request.getAddress(), port); return promise.setSuccess(singletonList(inetSocketAddress)); } - AddressResolver groupResolver = getGroupResolver(); - if (groupResolver != null) { - return RequestHostnameResolver.INSTANCE.resolve(groupResolver, unresolvedRemoteAddress, asyncHandler); - } else { - return RequestHostnameResolver.INSTANCE.resolve(request.getNameResolver(), unresolvedRemoteAddress, asyncHandler); - } + return resolveHostname(request, unresolvedRemoteAddress, asyncHandler); } } - private @Nullable AddressResolver getGroupResolver() { + private Future> resolveHostname(Request request, InetSocketAddress unresolvedRemoteAddress, AsyncHandler asyncHandler) { AddressResolverGroup group = channelManager.getAddressResolverGroup(); - return group != null ? group.getResolver(channelManager.getEventLoopGroup().next()) : null; + if (group != null) { + AddressResolver resolver = group.getResolver(channelManager.getEventLoopGroup().next()); + return RequestHostnameResolver.INSTANCE.resolve(resolver, unresolvedRemoteAddress, asyncHandler); + } + return RequestHostnameResolver.INSTANCE.resolve(request.getNameResolver(), unresolvedRemoteAddress, asyncHandler); } private NettyResponseFuture newNettyResponseFuture(Request request, AsyncHandler asyncHandler, NettyRequest nettyRequest, ProxyServer proxyServer) { diff --git a/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java b/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java index 5fa16436db..60a3d62ccf 100644 --- a/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java +++ b/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java @@ -27,7 +27,7 @@ import java.net.InetAddress; import java.net.InetSocketAddress; -import java.net.UnknownHostException; + import java.util.ArrayList; import java.util.List; @@ -126,21 +126,14 @@ protected void onSuccess(List socketAddresses) { @Override protected void onFailure(Throwable t) { - // Wrap non-UnknownHostException DNS errors for backward compatibility. - // The JDK resolver always threw UnknownHostException; Netty's async DNS - // resolver may throw DnsErrorCauseException for SERVFAIL or other errors. - Throwable cause = t instanceof UnknownHostException ? t : new UnknownHostException(hostname); - if (!(t instanceof UnknownHostException)) { - cause.initCause(t); - } try { - asyncHandler.onHostnameResolutionFailure(hostname, cause); + asyncHandler.onHostnameResolutionFailure(hostname, t); } catch (Exception e) { LOGGER.error("onHostnameResolutionFailure crashed", e); promise.tryFailure(e); return; } - promise.tryFailure(cause); + promise.tryFailure(t); } }); From 6be94cae8768819a2f072e8d9802d58c597ee6b6 Mon Sep 17 00:00:00 2001 From: Aayush Atharva <24762260+hyperxpro@users.noreply.github.com> Date: Sat, 11 Apr 2026 15:35:09 +0530 Subject: [PATCH 8/8] Update client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java --- .../test/java/org/asynchttpclient/AddressResolverGroupTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java b/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java index 5bd85eaccf..529478c64e 100644 --- a/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java +++ b/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 AsyncHttpClient Project. All rights reserved. + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.