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 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()); + } + } +}