diff --git a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java
index e6e9a1d913..6bdb2248d3 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,26 @@ 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}. 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.
+ *
+ * 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
+ */
+ @Nullable
+ default AddressResolverGroup getAddressResolverGroup() {
+ return null;
+ }
+
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..74f9937b54 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,25 @@ 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} (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
+ */
+ public Builder setAddressResolverGroup(@Nullable AddressResolverGroup addressResolverGroup) {
+ this.addressResolverGroup = addressResolverGroup;
+ return this;
+ }
+
public Builder setUseNativeTransport(boolean useNativeTransport) {
this.useNativeTransport = useNativeTransport;
return this;
@@ -1650,6 +1681,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/netty/channel/ChannelManager.java b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java
index 0af03dfd41..3db64e64ad 100755
--- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java
+++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java
@@ -53,6 +53,8 @@
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.util.Timer;
import io.netty.util.concurrent.DefaultThreadFactory;
@@ -82,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;
@@ -122,6 +125,7 @@ public class ChannelManager {
private final Bootstrap httpBootstrap;
private final Bootstrap wsBootstrap;
private final long handshakeTimeout;
+ private final @Nullable AddressResolverGroup addressResolverGroup;
private final ChannelPool channelPool;
private final ChannelGroup openChannels;
@@ -193,6 +197,9 @@ public ChannelManager(final AsyncHttpClientConfig config, Timer nettyTimer) {
httpBootstrap = newBootstrap(transportFactory, eventLoopGroup, config);
wsBootstrap = newBootstrap(transportFactory, eventLoopGroup, config);
+
+ // 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) {
@@ -412,6 +419,11 @@ private void doClose() {
}
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();
@@ -579,39 +591,27 @@ public Future getBootstrap(Uri uri, NameResolver nameRes
Bootstrap socksBootstrap = httpBootstrap.clone();
ChannelHandler httpBootstrapHandler = socksBootstrap.config().handler();
- nameResolver.resolve(proxy.getHost()).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());
- 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
@@ -624,6 +624,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).
*/
@@ -790,6 +819,14 @@ public EventLoopGroup getEventLoopGroup() {
return eventLoopGroup;
}
+ /**
+ * Return the {@link AddressResolverGroup} used for async DNS resolution, or {@code null}
+ * if per-request name resolvers should be used (legacy behavior).
+ */
+ public @Nullable 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..73679fd17e 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,8 @@
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.resolver.AddressResolverGroup;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.Timer;
import io.netty.util.concurrent.Future;
@@ -72,6 +74,7 @@
import org.asynchttpclient.resolver.RequestHostnameResolver;
import org.asynchttpclient.uri.Uri;
import org.asynchttpclient.ws.WebSocketUpgradeHandler;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -374,7 +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);
- return RequestHostnameResolver.INSTANCE.resolve(request.getNameResolver(), unresolvedRemoteAddress, asyncHandler);
+ return resolveHostname(request, unresolvedRemoteAddress, asyncHandler);
} else {
int port = uri.getExplicitPort();
@@ -385,10 +388,18 @@ private Future> resolveAddresses(Request request, Pr
// bypass resolution
InetSocketAddress inetSocketAddress = new InetSocketAddress(request.getAddress(), port);
return promise.setSuccess(singletonList(inetSocketAddress));
- } else {
- return RequestHostnameResolver.INSTANCE.resolve(request.getNameResolver(), unresolvedRemoteAddress, asyncHandler);
}
+ return resolveHostname(request, unresolvedRemoteAddress, asyncHandler);
+ }
+ }
+
+ private Future> resolveHostname(Request request, InetSocketAddress unresolvedRemoteAddress, AsyncHandler> asyncHandler) {
+ AddressResolverGroup group = channelManager.getAddressResolverGroup();
+ 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 b6330cf14a..60a3d62ccf 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;
@@ -26,6 +27,7 @@
import java.net.InetAddress;
import java.net.InetSocketAddress;
+
import java.util.ArrayList;
import java.util.List;
@@ -83,4 +85,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;
+ }
}
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..5bd85eaccf
--- /dev/null
+++ b/client/src/test/java/org/asynchttpclient/AddressResolverGroupTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.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 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() {
+ 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();
+ 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);
+ fail("Request to nonexistent host should have thrown an exception");
+ } catch (ExecutionException e) {
+ 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(GOOGLE_URL).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(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(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());
+ }
+ }
+}