From 490507e182e84a5307f1e0f18936fdd012267ea9 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Sun, 21 Jun 2026 19:28:15 +0000 Subject: [PATCH 1/2] Fall back to NIO instead of throwing when no native transport is available --- .../netty/channel/ChannelManager.java | 18 ++++++++++--- .../DefaultAsyncHttpClientTest.java | 25 +++++++++++++++++++ 2 files changed, 40 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 f2aa4b6534..09bba1aa22 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java @@ -98,6 +98,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.stream.Collectors; @@ -119,6 +120,9 @@ public class ChannelManager { public static final String HTTP2_MULTIPLEX = "http2-multiplex"; public static final String AHC_HTTP2_HANDLER = "ahc-http2"; private static final Logger LOGGER = LoggerFactory.getLogger(ChannelManager.class); + // Guards the one-time WARN emitted when a native transport was requested but is unavailable and we + // fall back to NIO. Logged once per JVM to avoid spamming logs when many clients are created. + private static final AtomicBoolean NATIVE_FALLBACK_WARNED = new AtomicBoolean(); private final AsyncHttpClientConfig config; private final SslEngineFactory sslEngineFactory; private final EventLoopGroup eventLoopGroup; @@ -212,10 +216,11 @@ public ChannelManager(final AsyncHttpClientConfig config, Timer nettyTimer) { } // If we're not running on Windows then we're probably running on Linux. - // We will check if Io_Uring is available or not. If available, return IoUringIncubatorTransportFactory. + // We will check if Io_Uring is available or not. If available, return IoUringTransportFactory. // Else // We will check if Epoll is available or not. If available, return EpollTransportFactory. - // If none of the condition matches then no native transport is available, and we will throw an exception. + // If none of these match then no native transport is available; instead of failing client + // construction we degrade gracefully to NIO (which is always available) and warn once. if (!PlatformDependent.isWindows()) { if (IoUringTransportFactory.isAvailable() && !config.isUseOnlyEpollNativeTransport()) { return new IoUringTransportFactory(); @@ -224,7 +229,14 @@ public ChannelManager(final AsyncHttpClientConfig config, Timer nettyTimer) { } } - throw new IllegalArgumentException("No suitable native transport (Epoll, Io_Uring or KQueue) available"); + // No suitable native transport (Epoll, Io_Uring or KQueue) on this platform. Native transport was + // requested but cannot be honored (e.g. Windows, a minimal image without the native libs, or the + // native library failed to load), so fall back to the portable NIO transport rather than throwing. + if (NATIVE_FALLBACK_WARNED.compareAndSet(false, true)) { + LOGGER.warn("Native transport requested (useNativeTransport=true) but no native transport " + + "(Epoll, Io_Uring or KQueue) is available on this platform; falling back to NIO."); + } + return NioTransportFactory.INSTANCE; } public static boolean isSslHandlerConfigured(ChannelPipeline pipeline) { diff --git a/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientTest.java b/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientTest.java index f2f89d3f94..71378475d1 100644 --- a/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientTest.java +++ b/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientTest.java @@ -16,9 +16,13 @@ package org.asynchttpclient; import io.github.artsok.RepeatedIfExceptionsTest; +import io.netty.channel.EventLoopGroup; import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.uring.IoUring; import io.netty.util.Timer; import org.asynchttpclient.cookie.CookieEvictionTask; import org.asynchttpclient.cookie.CookieStore; @@ -33,6 +37,7 @@ import static org.asynchttpclient.Dsl.config; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -75,6 +80,26 @@ public void testNativeTransportKQueueOnMacOs() throws Exception { } } + @RepeatedIfExceptionsTest(repeats = 5) + @EnabledOnOs(OS.LINUX) + public void testNativeTransportFallsBackToNioWhenNativeUnavailable() throws IOException { + // Requesting native transport must never fail client construction: when no native transport is + // available (Windows, a minimal image without the native libs, or native libs forced off via + // -Dio.netty.transport.noNative=true) the selector degrades gracefully to NIO instead of throwing. + // This assertion is meaningful in the force-native-disabled soak run; on a host where native IS + // available it documents that native is still selected (no regression). + AsyncHttpClientConfig config = config().setUseNativeTransport(true).build(); + try (DefaultAsyncHttpClient client = (DefaultAsyncHttpClient) asyncHttpClient(config)) { + EventLoopGroup group = client.channelManager().getEventLoopGroup(); + boolean nativeAvailable = Epoll.isAvailable() || IoUring.isAvailable(); + if (nativeAvailable) { + assertFalse(group instanceof NioEventLoopGroup, "native transport available -> expected a native event loop group"); + } else { + assertInstanceOf(NioEventLoopGroup.class, group, "no native transport available -> expected graceful NIO fallback"); + } + } + } + @RepeatedIfExceptionsTest(repeats = 5) public void testUseOnlyEpollNativeTransportButNativeTransportIsDisabled() { assertThrows(IllegalArgumentException.class, () -> config().setUseNativeTransport(false).setUseOnlyEpollNativeTransport(true).build()); From 9e22612c06c2d8f3237f5bf316cefd7a77373343 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Sun, 21 Jun 2026 19:40:31 +0000 Subject: [PATCH 2/2] Sync client module parent version with root (3.0.12-SNAPSHOT) --- client/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pom.xml b/client/pom.xml index acabd23c32..65271eaca6 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -19,7 +19,7 @@ org.asynchttpclient async-http-client-project - 3.0.11 + 3.0.12-SNAPSHOT 4.0.0