diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java
index c5a482c5d50..cbc2703458f 100644
--- a/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java
+++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java
@@ -1119,7 +1119,27 @@ public enum DefaultDriverOption implements DriverOption {
*
*
Value type: boolean
*/
- CLIENT_ROUTES_SHARD_AWARENESS_ENABLED("advanced.client-routes.shard-awarness-enabled");
+ CLIENT_ROUTES_SHARD_AWARENESS_ENABLED("advanced.client-routes.shard-awarness-enabled"),
+
+ /**
+ * Whether the driver may fall back to a direct connection when no client route is available for a
+ * node.
+ *
+ *
When {@code true} (the default), nodes that have no matching entry in {@code
+ * system.client_routes} — or whose proxy address cannot be reached or resolved — are contacted
+ * directly using their broadcast address. This preserves backward-compatible mixed proxy/direct
+ * topologies where some nodes are behind the private endpoint and others are not.
+ *
+ *
When {@code false}, the driver never falls back to a direct connection. Any node without a
+ * reachable route is treated as unreachable: it stays DOWN and the reconnection loop retries
+ * until a {@code CLIENT_ROUTES_CHANGE} event publishes the route. Note that setting this to
+ * {@code false} does not actively close existing direct connections; connection pools
+ * that were established before the flag was applied may continue to operate until naturally
+ * recycled.
+ *
+ *
Value type: boolean
+ */
+ CLIENT_ROUTES_DIRECT_CONNECTION_FALLBACK("advanced.client-routes.direct-connection-fallback");
private final String path;
diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java
index 60190fc1cce..e715ac94c30 100644
--- a/core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java
+++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java
@@ -400,6 +400,7 @@ protected static void fillWithDriverDefaults(OptionsMap map) {
// values) with no sensible scalar default, analogous to how CONFIG_RELOAD_INTERVAL is omitted.
map.put(TypedDriverOption.CLIENT_ROUTES_NATIVE_TRANSPORT_PORT, 9042);
map.put(TypedDriverOption.CLIENT_ROUTES_SHARD_AWARENESS_ENABLED, false);
+ map.put(TypedDriverOption.CLIENT_ROUTES_DIRECT_CONNECTION_FALLBACK, true);
}
@Immutable
diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java
index db5edb5b947..1060efe4cb3 100644
--- a/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java
+++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java
@@ -954,6 +954,15 @@ public String toString() {
new TypedDriverOption<>(
DefaultDriverOption.CLIENT_ROUTES_SHARD_AWARENESS_ENABLED, GenericType.BOOLEAN);
+ /**
+ * Whether the driver may fall back to a direct connection when no client route is available for a
+ * node. When {@code true} (default), nodes without a reachable route are contacted directly via
+ * their broadcast address. When {@code false}, no fallback is attempted and the node stays DOWN.
+ */
+ public static final TypedDriverOption CLIENT_ROUTES_DIRECT_CONNECTION_FALLBACK =
+ new TypedDriverOption<>(
+ DefaultDriverOption.CLIENT_ROUTES_DIRECT_CONNECTION_FALLBACK, GenericType.BOOLEAN);
+
private static Iterable> introspectBuiltInValues() {
try {
ImmutableList.Builder> result = ImmutableList.builder();
diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/ClientRoutesEndPoint.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/ClientRoutesEndPoint.java
index 15d825b2efc..18eb2c682fa 100644
--- a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/ClientRoutesEndPoint.java
+++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/ClientRoutesEndPoint.java
@@ -27,12 +27,17 @@
import java.net.SocketAddress;
import java.util.Objects;
import java.util.UUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class ClientRoutesEndPoint implements EndPoint {
+ private static final Logger LOG = LoggerFactory.getLogger(ClientRoutesEndPoint.class);
+
private final UUID hostId;
private final ClientRoutesTopologyMonitor topologyMonitor;
private final String metricPrefix;
@NonNull private final EndPoint fallbackEndPoint;
+ private final boolean directConnectionFallback;
/**
* @param topologyMonitor the topology monitor used to resolve the endpoint address on demand.
@@ -40,20 +45,24 @@ public class ClientRoutesEndPoint implements EndPoint {
* @param broadcastInetAddress the node's broadcast address (from system.peers or system.local),
* used to build a stable metric prefix. May be {@code null} if the address could not be
* determined, in which case the hostId is used as the metric prefix instead.
- * @param fallbackEndPoint the default endpoint to fall back to when {@code
- * topologyMonitor.resolve()} returns {@code null}, i.e. when this node is not accessed via a
- * cloud private endpoint. Must not be {@code null}.
+ * @param fallbackEndPoint the endpoint to use when {@code topologyMonitor.resolve()} returns
+ * {@code null} and {@code directConnectionFallback} is {@code true}. Always required.
+ * @param directConnectionFallback when {@code true}, {@link #resolve()} falls back to {@code
+ * fallbackEndPoint} if no client route is found. When {@code false}, throws instead, keeping
+ * the node DOWN until a route is published.
*/
public ClientRoutesEndPoint(
@NonNull ClientRoutesTopologyMonitor topologyMonitor,
@NonNull UUID hostId,
@Nullable InetAddress broadcastInetAddress,
- @NonNull EndPoint fallbackEndPoint) {
+ @NonNull EndPoint fallbackEndPoint,
+ boolean directConnectionFallback) {
this.topologyMonitor =
Objects.requireNonNull(topologyMonitor, "Topology monitor cannot be null");
this.hostId = Objects.requireNonNull(hostId, "HOST uuid cannot be null");
this.fallbackEndPoint =
Objects.requireNonNull(fallbackEndPoint, "Fallback endpoint cannot be null");
+ this.directConnectionFallback = directConnectionFallback;
this.metricPrefix = buildMetricPrefix(broadcastInetAddress, hostId);
}
@@ -73,7 +82,24 @@ public SocketAddress resolve() {
} catch (IOException e) {
throw new UncheckedIOException("DNS resolution failed for host_id=" + hostId, e);
}
- return fallbackEndPoint.resolve();
+ if (directConnectionFallback) {
+ // Default (backward-compatible) mode: fall back to the node's broadcast address.
+ // This supports mixed proxy/direct topologies where some nodes are behind the private
+ // endpoint and others are reached directly.
+ return fallbackEndPoint.resolve();
+ }
+ // direct-connection-fallback=false: the driver must not bypass the proxy infrastructure.
+ // The node will remain DOWN and the reconnection loop will retry until a
+ // CLIENT_ROUTES_CHANGE event populates the route.
+ LOG.warn(
+ "No client route entry found for host_id={}. "
+ + "The node will remain DOWN until a route is published via CLIENT_ROUTES_CHANGE.",
+ hostId);
+ throw new IllegalStateException(
+ "No client route entry found for host_id="
+ + hostId
+ + ". Direct connection fallback is disabled"
+ + " (advanced.client-routes.direct-connection-fallback = false).");
}
@Override
diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/ClientRoutesTopologyMonitor.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/ClientRoutesTopologyMonitor.java
index 569637e0873..756ea45c083 100644
--- a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/ClientRoutesTopologyMonitor.java
+++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/ClientRoutesTopologyMonitor.java
@@ -82,6 +82,7 @@ public class ClientRoutesTopologyMonitor extends DefaultTopologyMonitor {
private final String logPrefix;
private final AtomicReference