diff --git a/client/pom.xml b/client/pom.xml
index acabd23c3..65271eaca 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
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 f2aa4b653..09bba1aa2 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 f2f89d3f9..71378475d 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());