This is an automated email from the ASF dual-hosted git repository.

ptupitsyn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 4d034f44035 IGNITE-28303 Clients: do not retry authentication errors 
(#7955)
4d034f44035 is described below

commit 4d034f440351b87357aa5e961517b86833c4e796
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Tue Apr 14 16:01:26 2026 +0300

    IGNITE-28303 Clients: do not retry authentication errors (#7955)
    
    Fix authn error handing in .NET and Java clients:
    - Propagate authn exceptions correctly
    - Do not invoke `RetryPolicy` for those errors
---
 .../exception/handler/SqlExceptionHandler.java     | 24 ++++++-----
 .../client/ItThinClientAuthenticationTest.java     | 46 +++++++++++++---------
 .../internal/client/ItThinClientSqlTest.java       |  2 +-
 .../ignite/internal/client/ReliableChannel.java    |  4 +-
 .../ignite/internal/client/TcpClientChannel.java   | 11 ++++++
 .../ignite/client/ClientAuthenticationTest.java    | 18 +++++++++
 .../ignite/client/ClientKeyValueViewTest.java      | 13 +++---
 .../Apache.Ignite.Tests/BasicAuthenticatorTests.cs | 32 +++++++++++++++
 .../dotnet/Apache.Ignite/IgniteException.cs        |  2 +-
 .../Apache.Ignite/Internal/ClientFailoverSocket.cs |  6 +++
 .../dotnet/Apache.Ignite/Internal/ClientSocket.cs  |  2 +-
 11 files changed, 121 insertions(+), 39 deletions(-)

diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/SqlExceptionHandler.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/SqlExceptionHandler.java
index 4bdd8ae18d7..5419f91043d 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/SqlExceptionHandler.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/SqlExceptionHandler.java
@@ -31,6 +31,7 @@ import 
org.apache.ignite.internal.cli.core.style.component.ErrorUiComponent;
 import org.apache.ignite.internal.logger.IgniteLogger;
 import org.apache.ignite.internal.logger.Loggers;
 import org.apache.ignite.internal.util.ExceptionUtils;
+import org.apache.ignite.lang.ErrorGroups.Authentication;
 import org.apache.ignite.lang.ErrorGroups.Client;
 import org.apache.ignite.lang.ErrorGroups.Sql;
 import org.apache.ignite.lang.IgniteCheckedException;
@@ -54,6 +55,7 @@ public class SqlExceptionHandler implements 
ExceptionHandler<SQLException> {
 
     /** Default constructor. */
     private SqlExceptionHandler() {
+        
sqlExceptionMappers.put(Authentication.AUTHENTICATION_ERR_GROUP.groupCode(), 
SqlExceptionHandler::authnErrUiComponent);
         sqlExceptionMappers.put(Client.CLIENT_ERR_GROUP.groupCode(), 
SqlExceptionHandler::connectionErrUiComponent);
         sqlExceptionMappers.put(Sql.SQL_ERR_GROUP.groupCode(), 
SqlExceptionHandler::sqlErrUiComponent);
     }
@@ -66,15 +68,6 @@ public class SqlExceptionHandler implements 
ExceptionHandler<SQLException> {
         if (e.getCause() instanceof IgniteClientConnectionException) {
             IgniteClientConnectionException cause = 
(IgniteClientConnectionException) e.getCause();
 
-            InvalidCredentialsException invalidCredentialsException = 
findCause(cause, InvalidCredentialsException.class);
-            if (invalidCredentialsException != null) {
-                return ErrorUiComponent.builder()
-                        .header("Could not connect to node. Check 
authentication configuration")
-                        .details(invalidCredentialsException.getMessage())
-                        .verbose(extractCauseMessage(cause.getMessage()))
-                        .build();
-            }
-
             SSLHandshakeException sslHandshakeException = findCause(cause, 
SSLHandshakeException.class);
             if (sslHandshakeException != null) {
                 return ErrorUiComponent.builder()
@@ -90,6 +83,19 @@ public class SqlExceptionHandler implements 
ExceptionHandler<SQLException> {
         return fromIgniteException(CLIENT_CONNECTION_FAILED_MESSAGE, e);
     }
 
+    private static ErrorUiComponent authnErrUiComponent(IgniteException e) {
+        InvalidCredentialsException invalidCredentialsException = findCause(e, 
InvalidCredentialsException.class);
+        if (invalidCredentialsException != null) {
+            return ErrorUiComponent.builder()
+                    .header("Could not connect to node. Check authentication 
configuration")
+                    .details(invalidCredentialsException.getMessage())
+                    .verbose(extractCauseMessage(e.getMessage()))
+                    .build();
+        }
+
+        return fromIgniteException("Could not connect to node. Check 
authentication configuration", e);
+    }
+
     @Nullable
     private static <T extends Throwable> T findCause(Throwable e, Class<T> 
type) {
         while (e != null) {
diff --git 
a/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientAuthenticationTest.java
 
b/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientAuthenticationTest.java
index 4e8cc7b754e..59602cda976 100644
--- 
a/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientAuthenticationTest.java
+++ 
b/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientAuthenticationTest.java
@@ -35,6 +35,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import org.apache.ignite.client.BasicAuthenticator;
 import org.apache.ignite.client.IgniteClient;
+import org.apache.ignite.client.IgniteClient.Builder;
 import org.apache.ignite.internal.app.IgniteImpl;
 import org.apache.ignite.internal.configuration.ConfigurationRegistry;
 import 
org.apache.ignite.internal.security.authentication.basic.BasicAuthenticationProviderChange;
@@ -98,12 +99,17 @@ public class ItThinClientAuthenticationTest extends 
ItAbstractThinClientTest {
 
         assertThat(enableAuthentication, willCompleteSuccessfully());
 
-        clientWithAuth = IgniteClient.builder()
+        Builder builder = IgniteClient.builder()
                 .authenticator(basicAuthenticator)
-                .addresses(getClientAddresses().toArray(new String[0]))
-                .build();
+                .addresses(getClientAddresses().toArray(new String[0]));
+
+        await().untilAsserted(() -> {
+            try (IgniteClient c = builder.build()) {
+                assertThat(checkConnection(c), willCompleteSuccessfully());
+            }
+        });
 
-        await().untilAsserted(() -> checkConnection(clientWithAuth));
+        clientWithAuth = builder.build();
     }
 
     @AfterEach
@@ -174,7 +180,7 @@ public class ItThinClientAuthenticationTest extends 
ItAbstractThinClientTest {
     }
 
     @Test
-    void renameBasicProviderAndThenChangeUserPassword() {
+    void renameBasicProviderAndThenChangeUserPassword() throws 
InterruptedException {
         updateClusterConfiguration("ignite {\n"
                 + "security.authentication.providers.basic={\n"
                 + "type=basic,\n"
@@ -182,23 +188,25 @@ public class ItThinClientAuthenticationTest extends 
ItAbstractThinClientTest {
                 + "security.authentication.providers.default=null\n"
                 + "}");
 
-        try (IgniteClient client = IgniteClient.builder()
-                
.authenticator(BasicAuthenticator.builder().username("newuser").password("newpassword").build())
-                .addresses(getClientAddresses().toArray(new String[0]))
-                .build()) {
+        await().untilAsserted(() -> {
+            try (IgniteClient client = IgniteClient.builder()
+                    
.authenticator(BasicAuthenticator.builder().username("newuser").password("newpassword").build())
+                    .addresses(getClientAddresses().toArray(new String[0]))
+                    .build()) {
 
-            checkConnection(client);
+                checkConnection(client);
 
-            securityConfiguration.authentication().providers()
-                    .get("basic")
-                    .change(change -> {
-                        change.convert(BasicAuthenticationProviderChange.class)
-                                .changeUsers()
-                                .update("newuser", user -> 
user.changePassword("newpassword-changed"));
-                    }).join();
+                securityConfiguration.authentication().providers()
+                        .get("basic")
+                        .change(change -> {
+                            
change.convert(BasicAuthenticationProviderChange.class)
+                                    .changeUsers()
+                                    .update("newuser", user -> 
user.changePassword("newpassword-changed"));
+                        }).join();
 
-            await().until(() -> checkConnection(client), 
willThrowWithCauseOrSuppressed(InvalidCredentialsException.class));
-        }
+                await().until(() -> checkConnection(client), 
willThrowWithCauseOrSuppressed(InvalidCredentialsException.class));
+            }
+        });
     }
 
     /**
diff --git 
a/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientSqlTest.java
 
b/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientSqlTest.java
index 9115b6bc7a6..254e69ad6db 100644
--- 
a/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientSqlTest.java
+++ 
b/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientSqlTest.java
@@ -626,7 +626,7 @@ public class ItThinClientSqlTest extends 
ItAbstractThinClientTest {
                 IgniteException.class,
                 () -> client().sql().execute((Transaction) null, 
Mapper.of(Pojo.class), query));
 
-        assertEquals("Failed to deserialize server response for op 50: No 
mapped object field found for column 'FOO'", e.getMessage());
+        assertEquals("No mapped object field found for column 'FOO'", 
e.getMessage());
     }
 
     @Test
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/ReliableChannel.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/ReliableChannel.java
index f5774ea0b03..041f32997d4 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/ReliableChannel.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/ReliableChannel.java
@@ -73,6 +73,7 @@ import org.apache.ignite.internal.hlc.HybridTimestampTracker;
 import org.apache.ignite.internal.logger.IgniteLogger;
 import org.apache.ignite.internal.thread.NamedThreadFactory;
 import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.lang.ErrorGroups.Authentication;
 import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.network.ClusterNode;
 import org.jetbrains.annotations.Nullable;
@@ -779,7 +780,8 @@ public final class ReliableChannel implements AutoCloseable 
{
             return false;
         }
 
-        if (exception.code() == CLUSTER_ID_MISMATCH_ERR) {
+        if (exception.code() == CLUSTER_ID_MISMATCH_ERR
+                || exception.groupCode() == 
Authentication.AUTHENTICATION_ERR_GROUP.groupCode()) {
             return false;
         }
 
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
index 5c198f1da98..6ec7fdac4c4 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
@@ -77,6 +77,7 @@ import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.internal.util.ViewUtils;
 import org.apache.ignite.lang.ErrorGroups.Table;
 import org.apache.ignite.lang.IgniteException;
+import org.apache.ignite.lang.TraceableException;
 import org.apache.ignite.network.NetworkAddress;
 import org.apache.ignite.sql.SqlBatchException;
 import org.apache.ignite.tx.TransactionException;
@@ -533,6 +534,10 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
 
             return null;
         } catch (Throwable e) {
+            if (e instanceof TraceableException) {
+                throw sneakyThrow(e);
+            }
+
             log.error("Failed to deserialize server response [remoteAddress=" 
+ cfg.getAddress() + ", opCode=" + opCode + "]: "
                     + e.getMessage(), e);
 
@@ -754,7 +759,13 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
                 metrics.handshakesFailedTimeoutIncrement();
                 throw new IgniteClientConnectionException(CONNECTION_ERR, 
"Handshake timeout", endpoint(), err);
             }
+
             metrics.handshakesFailedIncrement();
+
+            if (err instanceof TraceableException) {
+                throw new 
IgniteClientConnectionException(((TraceableException) err).code(), "Handshake 
error", endpoint(), err);
+            }
+
             throw new IgniteClientConnectionException(CONNECTION_ERR, 
"Handshake error", endpoint(), err);
         });
     }
diff --git 
a/modules/client/src/test/java/org/apache/ignite/client/ClientAuthenticationTest.java
 
b/modules/client/src/test/java/org/apache/ignite/client/ClientAuthenticationTest.java
index 96704bffb46..2b71f45e4d4 100644
--- 
a/modules/client/src/test/java/org/apache/ignite/client/ClientAuthenticationTest.java
+++ 
b/modules/client/src/test/java/org/apache/ignite/client/ClientAuthenticationTest.java
@@ -23,6 +23,7 @@ import static 
org.apache.ignite.internal.security.authentication.SecurityConfigu
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
 import static org.apache.ignite.internal.util.IgniteUtils.closeAll;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.util.UUID;
 import org.apache.ignite.client.fakes.FakeIgnite;
@@ -93,6 +94,23 @@ public class ClientAuthenticationTest extends 
BaseIgniteAbstractTest {
         client = 
startClient(BasicAuthenticator.builder().username(DEFAULT_USERNAME).password(DEFAULT_PASSWORD).build());
     }
 
+    @Test
+    public void testBadCredentialsAreNotRetried() {
+        server = startServer(true);
+
+        BasicAuthenticator authenticator = 
BasicAuthenticator.builder().username("u").password("wrong").build();
+
+        var builder = IgniteClient.builder()
+                .addresses("127.0.0.1:" + server.port())
+                .authenticator(authenticator)
+                .retryPolicy(new RetryLimitPolicy().retryLimit(5));
+
+        IgniteTestUtils.assertThrowsWithCause(builder::build, 
InvalidCredentialsException.class, "Authentication failed");
+
+        // The server should have received exactly one connection attempt, no 
retries.
+        assertEquals(1, server.metrics().sessionsRejected());
+    }
+
     private IgniteClient startClient(@Nullable IgniteClientAuthenticator 
authenticator) {
         return IgniteClient.builder()
                 .addresses("127.0.0.1:" + server.port())
diff --git 
a/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java
 
b/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java
index 72c68f3e3df..de04be723db 100644
--- 
a/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java
+++ 
b/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java
@@ -579,32 +579,31 @@ public class ClientKeyValueViewTest extends 
AbstractClientTableTest {
 
     @Test
     public void testGetNullValueThrows() {
-        testNullValueThrows(view -> view.get(null, DEFAULT_ID), "getNullable", 
12);
+        testNullValueThrows(view -> view.get(null, DEFAULT_ID), "getNullable");
     }
 
     @Test
     public void testGetAndPutNullValueThrows() {
-        testNullValueThrows(view -> view.getAndPut(null, DEFAULT_ID, 
DEFAULT_NAME), "getNullableAndPut", 16);
+        testNullValueThrows(view -> view.getAndPut(null, DEFAULT_ID, 
DEFAULT_NAME), "getNullableAndPut");
     }
 
     @Test
     public void testGetAndRemoveNullValueThrows() {
-        testNullValueThrows(view -> view.getAndRemove(null, DEFAULT_ID), 
"getNullableAndRemove", 32);
+        testNullValueThrows(view -> view.getAndRemove(null, DEFAULT_ID), 
"getNullableAndRemove");
     }
 
     @Test
     public void testGetAndReplaceNullValueThrows() {
-        testNullValueThrows(view -> view.getAndReplace(null, DEFAULT_ID, 
DEFAULT_NAME), "getNullableAndReplace", 26);
+        testNullValueThrows(view -> view.getAndReplace(null, DEFAULT_ID, 
DEFAULT_NAME), "getNullableAndReplace");
     }
 
-    private void testNullValueThrows(Consumer<KeyValueView<Long, String>> run, 
String methodName, int op) {
+    private void testNullValueThrows(Consumer<KeyValueView<Long, String>> run, 
String methodName) {
         KeyValueView<Long, String> primitiveView = 
defaultTable().keyValueView(Mapper.of(Long.class), Mapper.of(String.class));
         primitiveView.put(null, DEFAULT_ID, null);
 
         var ex = assertThrowsWithCause(() -> run.accept(primitiveView), 
UnexpectedNullValueException.class);
         assertEquals(
-                format("Failed to deserialize server response for op {}: Got 
unexpected null value: use `{}` sibling method instead.",
-                        op, methodName),
+                format("Got unexpected null value: use `{}` sibling method 
instead.", methodName),
                 ex.getMessage());
     }
 
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/BasicAuthenticatorTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/BasicAuthenticatorTests.cs
index 711a1368e77..11a93ab75e0 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/BasicAuthenticatorTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/BasicAuthenticatorTests.cs
@@ -64,6 +64,27 @@ public class BasicAuthenticatorTests : IgniteTestsBase
         Assert.AreEqual(ErrorGroups.Authentication.InvalidCredentials, 
invalidCredentialsException.Code);
     }
 
+    [Test]
+    public async Task TestBadCredentialsAreNotRetried()
+    {
+        await EnableAuthn(true);
+
+        var retryPolicy = new TestRetryPolicy();
+        var cfg = new IgniteClientConfiguration(GetConfig())
+        {
+            RetryPolicy = retryPolicy,
+            Authenticator = new BasicAuthenticator
+            {
+                Username = "user-1",
+                Password = "wrong"
+            }
+        };
+
+        var ex = Assert.ThrowsAsync<IgniteClientConnectionException>(async () 
=> await IgniteClient.StartAsync(cfg));
+        Assert.IsInstanceOf<InvalidCredentialsException>(ex.InnerException);
+        Assert.AreEqual(0, retryPolicy.InvokeCount, "Retry policy should not 
be invoked");
+    }
+
     [Test]
     public async Task TestAuthnOnClientAndServer()
     {
@@ -150,4 +171,15 @@ public class BasicAuthenticatorTests : IgniteTestsBase
 
         _authnEnabled = enable;
     }
+
+    private class TestRetryPolicy : RetryLimitPolicy
+    {
+        public int InvokeCount { get; private set; }
+
+        public override bool ShouldRetry(IRetryPolicyContext context)
+        {
+            InvokeCount++;
+            return base.ShouldRetry(context);
+        }
+    }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite/IgniteException.cs 
b/modules/platforms/dotnet/Apache.Ignite/IgniteException.cs
index be517e3ea73..c1be493a438 100644
--- a/modules/platforms/dotnet/Apache.Ignite/IgniteException.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/IgniteException.cs
@@ -40,7 +40,7 @@ namespace Apache.Ignite
         public IgniteException(Guid traceId, int code, string? message, 
Exception? innerException = null)
             : base(message, innerException)
         {
-            TraceId = traceId;
+            TraceId = (innerException as IgniteException)?.TraceId ?? traceId;
             Code = code;
         }
 
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs
index cbeca14e367..1ba4ed359e5 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs
@@ -881,6 +881,12 @@ namespace Apache.Ignite.Internal
                 return false;
             }
 
+            if (e is IgniteException { GroupCode: 
ErrorGroups.Authentication.GroupCode })
+            {
+                // Authentication errors should not be retried.
+                return false;
+            }
+
             if (retryPolicy is null or RetryNonePolicy)
             {
                 return false;
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
index e8b5e58f9ec..4fcf9d675e0 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
@@ -277,7 +277,7 @@ namespace Apache.Ignite.Internal
                 }
 
                 throw new IgniteClientConnectionException(
-                    ErrorGroups.Client.Connection,
+                    (ex as IgniteException)?.Code ?? 
ErrorGroups.Client.Connection,
                     "Failed to connect to endpoint: " + endPoint.EndPoint,
                     ex);
             }

Reply via email to