diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs index 8046ab045..2d0daec25 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs @@ -324,7 +324,10 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM } catch (Exception ex) { - if (ex is OperationCanceledException oce) + // When terminating by Heartbeat or DisposeAsync, a RpcException with a Status of Canceled is thrown. + // If `ex.InnerException` is OperationCanceledException` and `subscriptionToken.IsCancellationRequested` is true, it is treated as a normal cancellation. + if ((ex is OperationCanceledException oce) || + (ex is RpcException { InnerException: OperationCanceledException } && subscriptionToken.IsCancellationRequested)) { if (heartbeatManager.TimeoutToken.IsCancellationRequested) { @@ -332,6 +335,7 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM } return; } + const string msg = "An error occurred while subscribing to messages."; // log post on main thread. if (syncContext != null) diff --git a/src/MagicOnion.Client/StreamingHubClientBase.cs b/src/MagicOnion.Client/StreamingHubClientBase.cs index 8046ab045..2d0daec25 100644 --- a/src/MagicOnion.Client/StreamingHubClientBase.cs +++ b/src/MagicOnion.Client/StreamingHubClientBase.cs @@ -324,7 +324,10 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM } catch (Exception ex) { - if (ex is OperationCanceledException oce) + // When terminating by Heartbeat or DisposeAsync, a RpcException with a Status of Canceled is thrown. + // If `ex.InnerException` is OperationCanceledException` and `subscriptionToken.IsCancellationRequested` is true, it is treated as a normal cancellation. + if ((ex is OperationCanceledException oce) || + (ex is RpcException { InnerException: OperationCanceledException } && subscriptionToken.IsCancellationRequested)) { if (heartbeatManager.TimeoutToken.IsCancellationRequested) { @@ -332,6 +335,7 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM } return; } + const string msg = "An error occurred while subscribing to messages."; // log post on main thread. if (syncContext != null) diff --git a/tests/MagicOnion.Client.Tests/ChannelAsyncStreamReader.cs b/tests/MagicOnion.Client.Tests/ChannelAsyncStreamReader.cs index dbcf726e9..d71044916 100644 --- a/tests/MagicOnion.Client.Tests/ChannelAsyncStreamReader.cs +++ b/tests/MagicOnion.Client.Tests/ChannelAsyncStreamReader.cs @@ -15,14 +15,25 @@ public ChannelAsyncStreamReader(Channel channel) public async Task MoveNext(CancellationToken cancellationToken) { - if (await reader.WaitToReadAsync(cancellationToken)) + try { - if (reader.TryRead(out var item)) + if (await reader.WaitToReadAsync(cancellationToken)) { - Current = item; - return true; + if (reader.TryRead(out var item)) + { + Current = item; + return true; + } } } + catch (OperationCanceledException e) + { + throw new RpcException(new Status(StatusCode.Cancelled, e.Message, e)); + } + catch (Exception e) + { + throw new RpcException(new Status(StatusCode.Unknown, e.Message, e)); + } return false; } diff --git a/tests/MagicOnion.Client.Tests/StreamingHubTest.cs b/tests/MagicOnion.Client.Tests/StreamingHubTest.cs index d127dd924..0a5236ef5 100644 --- a/tests/MagicOnion.Client.Tests/StreamingHubTest.cs +++ b/tests/MagicOnion.Client.Tests/StreamingHubTest.cs @@ -617,7 +617,7 @@ public async Task WaitForDisconnectAsync_TimedOut() // Assert Assert.Equal(DisconnectionType.TimedOut, disconnectionReason.Type); - Assert.IsType(disconnectionReason.Exception); + Assert.IsType(disconnectionReason.Exception); } [Fact] @@ -635,7 +635,7 @@ public async Task WaitForDisconnectAsync_Faulted() // Assert Assert.Equal(DisconnectionType.Faulted, disconnectionReason.Type); - Assert.IsType(disconnectionReason.Exception); + Assert.IsType(disconnectionReason.Exception); } [Fact] diff --git a/tests/MagicOnion.Integration.Tests/StreamingHubTest.cs b/tests/MagicOnion.Integration.Tests/StreamingHubTest.cs index 83b85d7e0..29906bbf1 100644 --- a/tests/MagicOnion.Integration.Tests/StreamingHubTest.cs +++ b/tests/MagicOnion.Integration.Tests/StreamingHubTest.cs @@ -659,6 +659,50 @@ public async Task CustomMethodId_Receiver(TestStreamingHubClientFactory clientFa // Assert receiver.Received().Receiver_CustomMethodId(); } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task WaitForDisconnectAsync_CompletedNormally(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + + var receiver = Substitute.For(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + await client.DisposeAsync(); + var reason = await client.WaitForDisconnectAsync(); + + // Assert + reason.Type.Should().Be(DisconnectionType.CompletedNormally); + reason.Exception.Should().BeNull(); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task WaitForDisconnectAsync_Faulted(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + + var receiver = Substitute.For(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + try + { + await client.DisconnectFromServerAsync(); + } + catch { } + var reason = await client.WaitForDisconnectAsync(); + + // Assert + reason.Type.Should().Be(DisconnectionType.Faulted); + reason.Exception.Should().NotBeNull(); + } } public class StreamingHubTestHub : StreamingHubBase, IStreamingHubTestHub @@ -870,6 +914,12 @@ public Task CustomMethodId() { return Task.CompletedTask; } + + public Task DisconnectFromServerAsync() + { + this.Context.CallContext.GetHttpContext().Abort(); + return Task.CompletedTask; + } } public interface IStreamingHubTestHubReceiver @@ -940,4 +990,6 @@ public interface IStreamingHubTestHub : IStreamingHub