diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index f91746f99f50..d4aae23a2753 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -113,8 +113,17 @@ public ValueTask InvokeNewAsync(string identifier, Cancellat if (DefaultAsyncTimeout.HasValue) { using var cts = new CancellationTokenSource(DefaultAsyncTimeout.Value); - // We need to await here due to the using - return await InvokeAsync(targetInstanceId, identifier, callType, cts.Token, args); + + try + { + // We need to await here due to the using + return await InvokeAsync(targetInstanceId, identifier, callType, cts.Token, args); + } + catch (OperationCanceledException) when (cts.Token.IsCancellationRequested) + { + // This was cancelled due to our timeout, throw a more meaningful exception + throw new TimeoutException("A JavaScript interop call timed out. Consider increasing the timeout duration if the operation is expected to take longer."); + } } return await InvokeAsync(targetInstanceId, identifier, callType, CancellationToken.None, args); diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs index 65ce56f65ac9..cee3584a25c1 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs @@ -37,7 +37,7 @@ public void DispatchesAsyncCallsWithDistinctAsyncHandles() } [Fact] - public async Task InvokeAsync_CancelsAsyncTask_AfterDefaultTimeout() + public async Task InvokeAsync_ThrowsTimeoutException_AfterDefaultTimeout() { // Arrange var runtime = new TestJSRuntime(); @@ -47,7 +47,7 @@ public async Task InvokeAsync_CancelsAsyncTask_AfterDefaultTimeout() var task = runtime.InvokeAsync("test identifier 1", "arg1", 123, true); // Assert - await Assert.ThrowsAsync(async () => await task); + await Assert.ThrowsAsync(async () => await task); } [Fact] @@ -82,6 +82,24 @@ public async Task InvokeAsync_CancelsAsyncTasksWhenCancellationTokenFires() await Assert.ThrowsAsync(async () => await task); } + [Fact] + public async Task InvokeAsync_CancelsWithTaskCanceledException_WhenCancellationTokenFiresBeforeTimeout() + { + // Arrange + using var cts = new CancellationTokenSource(); + var runtime = new TestJSRuntime(); + // Set a long timeout, but cancel before it fires + runtime.DefaultTimeout = TimeSpan.FromSeconds(10); + + // Act + var task = runtime.InvokeAsync("test identifier 1", cts.Token, new object[] { "arg1", 123, true }); + + cts.Cancel(); + + // Assert - Should throw TaskCanceledException, not TimeoutException + await Assert.ThrowsAsync(async () => await task); + } + [Fact] public async Task InvokeAsync_DoesNotStartWorkWhenCancellationHasBeenRequested() {