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 extends Channel, ? extends EventLoopGroup> 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 extends DatagramChannel> 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 extends DatagramChannel> 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 extends DatagramChannel> 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 extends DatagramChannel> 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 extends DatagramChannel> 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 extends Channel, ? extends EventLoopGroup> 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 extends DatagramChannel> 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 extends DatagramChannel> 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 extends DatagramChannel> 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 extends DatagramChannel> 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 extends DatagramChannel> 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.