diff --git a/src/NATS.Client.Core/Internal/UserCredentials.cs b/src/NATS.Client.Core/Internal/UserCredentials.cs index 67ff30234..6f14ce311 100644 --- a/src/NATS.Client.Core/Internal/UserCredentials.cs +++ b/src/NATS.Client.Core/Internal/UserCredentials.cs @@ -159,7 +159,7 @@ internal async Task AuthenticateAsync(ClientOpts opts, ServerInfo? info, NatsUri using var reader = new StreamReader(path); return ParseCreds(reader); } - catch (NatsException e) + catch (Exception e) { throw new NatsException($"Error loading creds file '{path}': {e.Message}", e); } diff --git a/src/NATS.Client.Core/NatsConnection.cs b/src/NATS.Client.Core/NatsConnection.cs index 111709253..b814a3676 100644 --- a/src/NATS.Client.Core/NatsConnection.cs +++ b/src/NATS.Client.Core/NatsConnection.cs @@ -394,9 +394,26 @@ private async ValueTask InitialConnectAsync() throw new NatsException($"URI {uri} requires TLS but TlsMode is set to Disable"); } - if (!Opts.AuthOpts.IsAnonymous) + try + { + if (!Opts.AuthOpts.IsAnonymous) + { + _userCredentials = new UserCredentials(Opts.AuthOpts); + } + } + catch (Exception ex) { - _userCredentials = new UserCredentials(Opts.AuthOpts); + var exception = ex as NatsException ?? new NatsException($"Failed to load credentials: {ex.Message}", ex); + lock (_gate) + { + ConnectionState = NatsConnectionState.Closed; // allow retry connect + + // throw for the waiter + _waitForOpenConnection.TrySetObservedException(exception); + _waitForOpenConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + + throw exception; } var attemptedUris = new List(); diff --git a/tests/NATS.Client.CoreUnit.Tests/NatsConnectionAuthTests.cs b/tests/NATS.Client.CoreUnit.Tests/NatsConnectionAuthTests.cs new file mode 100644 index 000000000..221d03dfa --- /dev/null +++ b/tests/NATS.Client.CoreUnit.Tests/NatsConnectionAuthTests.cs @@ -0,0 +1,27 @@ +namespace NATS.Client.CoreUnit.Tests; + +public class NatsConnectionAuthTests +{ + [Fact] + public async Task ConnectAsync_MissingCredsFile_SecondCallDoesNotHang() + { + var missingPath = Path.Combine(Path.GetTempPath(), $"nats-missing-{Guid.NewGuid():N}.creds"); + var opts = NatsOpts.Default with + { + Url = "nats://127.0.0.1:4222", + RetryOnInitialConnect = false, + AuthOpts = NatsAuthOpts.Default with { CredsFile = missingPath }, + }; + + await using var nats = new NatsConnection(opts); + + var first = async () => await nats.ConnectAsync(); + await first.Should().ThrowAsync(); + + var secondTask = nats.ConnectAsync().AsTask(); + var completed = await Task.WhenAny(secondTask, Task.Delay(TimeSpan.FromSeconds(5))); + completed.Should().BeSameAs(secondTask, "second ConnectAsync must not hang"); + var second = async () => await secondTask; + await second.Should().ThrowAsync(); + } +}