From e8c7fb38b4579c8209dfc5d902b20805c8b12dc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:13:13 +0000 Subject: [PATCH 1/4] Initial plan From 941f88ebb12712f27960fe963de506c4cd7a6307 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:35:26 +0000 Subject: [PATCH 2/4] Implement JSTimeoutException and timeout handling in ComponentBase Co-authored-by: ilonatommy <32700855+ilonatommy@users.noreply.github.com> --- .../Components/src/ComponentBase.cs | 11 ++++++++ .../Components/test/ComponentBaseTest.cs | 19 +++++++++++++ .../Microsoft.JSInterop/src/JSRuntime.cs | 13 +++++++-- .../src/JSTimeoutException.cs | 27 +++++++++++++++++++ .../src/PublicAPI.Unshipped.txt | 3 +++ .../Microsoft.JSInterop/test/JSRuntimeTest.cs | 4 +-- 6 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 src/JSInterop/Microsoft.JSInterop/src/JSTimeoutException.cs diff --git a/src/Components/Components/src/ComponentBase.cs b/src/Components/Components/src/ComponentBase.cs index 5de04ae8d70b..5846890d841d 100644 --- a/src/Components/Components/src/ComponentBase.cs +++ b/src/Components/Components/src/ComponentBase.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.JSInterop; namespace Microsoft.AspNetCore.Components; @@ -289,6 +290,11 @@ private async Task RunInitAndSetParametersAsync() { await task; } + catch (JSTimeoutException) + { + // Let JSTimeoutException bubble up to provide meaningful error information + throw; + } catch // avoiding exception filters for AOT runtime support { // Ignore exceptions from task cancellations. @@ -332,6 +338,11 @@ private async Task CallStateHasChangedOnAsyncCompletion(Task task) { await task; } + catch (JSTimeoutException) + { + // Let JSTimeoutException bubble up to provide meaningful error information + throw; + } catch // avoiding exception filters for AOT runtime support { // Ignore exceptions from task cancellations, but don't bother issuing a state change. diff --git a/src/Components/Components/test/ComponentBaseTest.cs b/src/Components/Components/test/ComponentBaseTest.cs index b8a928a0ec41..64ada3629388 100644 --- a/src/Components/Components/test/ComponentBaseTest.cs +++ b/src/Components/Components/test/ComponentBaseTest.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Test.Helpers; +using Microsoft.JSInterop; namespace Microsoft.AspNetCore.Components.Test; @@ -463,6 +464,24 @@ public async Task RenderRootComponentAsync_ReportsErrorDuringOnParameterSetAsync Assert.Same(expected, actual); } + [Fact] + public async Task ComponentBase_AllowsJSTimeoutExceptionToBubbleUp() + { + // Arrange + var renderer = new TestRenderer(); + var component = new TestComponent(); + + var timeoutException = new JSTimeoutException("Test timeout"); + component.OnParametersSetAsyncLogic = _ => Task.FromException(timeoutException); + + // Act & Assert + var componentId = renderer.AssignRootComponentId(component); + var actual = await Assert.ThrowsAsync(() => renderer.RenderRootComponentAsync(componentId)); + + // Assert + Assert.Same(timeoutException, actual); + } + private class TestComponent : ComponentBase { public bool RunsBaseOnInit { get; set; } = true; diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index f91746f99f50..f8524c9dc3c9 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 JSTimeoutException("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/src/JSTimeoutException.cs b/src/JSInterop/Microsoft.JSInterop/src/JSTimeoutException.cs new file mode 100644 index 000000000000..d1f18396e0fd --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/JSTimeoutException.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.JSInterop; + +/// +/// Represents errors that occur when a JavaScript interop call times out. +/// +public class JSTimeoutException : JSException +{ + /// + /// Constructs an instance of . + /// + /// The exception message. + public JSTimeoutException(string message) : base(message) + { + } + + /// + /// Constructs an instance of . + /// + /// The exception message. + /// The inner exception. + public JSTimeoutException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index b6c32d826997..cc2cfa9777e0 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -54,6 +54,9 @@ Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, object?[]? args Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.JSRuntime.SetValueAsync(string! identifier, TValue value) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.JSRuntime.SetValueAsync(string! identifier, TValue value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.JSTimeoutException +Microsoft.JSInterop.JSTimeoutException.JSTimeoutException(string! message) -> void +Microsoft.JSInterop.JSTimeoutException.JSTimeoutException(string! message, System.Exception! innerException) -> void static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, params object?[]? args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.TimeSpan timeout, object?[]? args) -> System.Threading.Tasks.ValueTask diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs index 65ce56f65ac9..b03cf0e9160d 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_ThrowsJSTimeoutException_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] From 169cab9db43610e64e08496073f63f90bc341f6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:55:26 +0000 Subject: [PATCH 3/4] Replace JSTimeoutException with TimeoutException per feedback Co-authored-by: ilonatommy <32700855+ilonatommy@users.noreply.github.com> --- .../Components/src/ComponentBase.cs | 11 -------- .../Components/test/ComponentBaseTest.cs | 19 ------------- .../Microsoft.JSInterop/src/JSRuntime.cs | 2 +- .../src/JSTimeoutException.cs | 27 ------------------- .../src/PublicAPI.Unshipped.txt | 3 --- .../Microsoft.JSInterop/test/JSRuntimeTest.cs | 4 +-- 6 files changed, 3 insertions(+), 63 deletions(-) delete mode 100644 src/JSInterop/Microsoft.JSInterop/src/JSTimeoutException.cs diff --git a/src/Components/Components/src/ComponentBase.cs b/src/Components/Components/src/ComponentBase.cs index 5846890d841d..5de04ae8d70b 100644 --- a/src/Components/Components/src/ComponentBase.cs +++ b/src/Components/Components/src/ComponentBase.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Components.Rendering; -using Microsoft.JSInterop; namespace Microsoft.AspNetCore.Components; @@ -290,11 +289,6 @@ private async Task RunInitAndSetParametersAsync() { await task; } - catch (JSTimeoutException) - { - // Let JSTimeoutException bubble up to provide meaningful error information - throw; - } catch // avoiding exception filters for AOT runtime support { // Ignore exceptions from task cancellations. @@ -338,11 +332,6 @@ private async Task CallStateHasChangedOnAsyncCompletion(Task task) { await task; } - catch (JSTimeoutException) - { - // Let JSTimeoutException bubble up to provide meaningful error information - throw; - } catch // avoiding exception filters for AOT runtime support { // Ignore exceptions from task cancellations, but don't bother issuing a state change. diff --git a/src/Components/Components/test/ComponentBaseTest.cs b/src/Components/Components/test/ComponentBaseTest.cs index 64ada3629388..b8a928a0ec41 100644 --- a/src/Components/Components/test/ComponentBaseTest.cs +++ b/src/Components/Components/test/ComponentBaseTest.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Test.Helpers; -using Microsoft.JSInterop; namespace Microsoft.AspNetCore.Components.Test; @@ -464,24 +463,6 @@ public async Task RenderRootComponentAsync_ReportsErrorDuringOnParameterSetAsync Assert.Same(expected, actual); } - [Fact] - public async Task ComponentBase_AllowsJSTimeoutExceptionToBubbleUp() - { - // Arrange - var renderer = new TestRenderer(); - var component = new TestComponent(); - - var timeoutException = new JSTimeoutException("Test timeout"); - component.OnParametersSetAsyncLogic = _ => Task.FromException(timeoutException); - - // Act & Assert - var componentId = renderer.AssignRootComponentId(component); - var actual = await Assert.ThrowsAsync(() => renderer.RenderRootComponentAsync(componentId)); - - // Assert - Assert.Same(timeoutException, actual); - } - private class TestComponent : ComponentBase { public bool RunsBaseOnInit { get; set; } = true; diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index f8524c9dc3c9..d4aae23a2753 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -122,7 +122,7 @@ public ValueTask InvokeNewAsync(string identifier, Cancellat catch (OperationCanceledException) when (cts.Token.IsCancellationRequested) { // This was cancelled due to our timeout, throw a more meaningful exception - throw new JSTimeoutException("A JavaScript interop call timed out. Consider increasing the timeout duration if the operation is expected to take longer."); + throw new TimeoutException("A JavaScript interop call timed out. Consider increasing the timeout duration if the operation is expected to take longer."); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSTimeoutException.cs b/src/JSInterop/Microsoft.JSInterop/src/JSTimeoutException.cs deleted file mode 100644 index d1f18396e0fd..000000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/JSTimeoutException.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.JSInterop; - -/// -/// Represents errors that occur when a JavaScript interop call times out. -/// -public class JSTimeoutException : JSException -{ - /// - /// Constructs an instance of . - /// - /// The exception message. - public JSTimeoutException(string message) : base(message) - { - } - - /// - /// Constructs an instance of . - /// - /// The exception message. - /// The inner exception. - public JSTimeoutException(string message, Exception innerException) : base(message, innerException) - { - } -} \ No newline at end of file diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index cc2cfa9777e0..b6c32d826997 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -54,9 +54,6 @@ Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, object?[]? args Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.JSRuntime.SetValueAsync(string! identifier, TValue value) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.JSRuntime.SetValueAsync(string! identifier, TValue value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -Microsoft.JSInterop.JSTimeoutException -Microsoft.JSInterop.JSTimeoutException.JSTimeoutException(string! message) -> void -Microsoft.JSInterop.JSTimeoutException.JSTimeoutException(string! message, System.Exception! innerException) -> void static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, params object?[]? args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.TimeSpan timeout, object?[]? args) -> System.Threading.Tasks.ValueTask diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs index b03cf0e9160d..9f46984bcdf9 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_ThrowsJSTimeoutException_AfterDefaultTimeout() + public async Task InvokeAsync_ThrowsTimeoutException_AfterDefaultTimeout() { // Arrange var runtime = new TestJSRuntime(); @@ -47,7 +47,7 @@ public async Task InvokeAsync_ThrowsJSTimeoutException_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] From 0a75d59cd0e0746173d888954324ae4950b87a5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Jul 2025 11:14:10 +0000 Subject: [PATCH 4/4] Add test for cancellation token taking precedence over timeout Co-authored-by: ilonatommy <32700855+ilonatommy@users.noreply.github.com> --- .../Microsoft.JSInterop/test/JSRuntimeTest.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs index 9f46984bcdf9..cee3584a25c1 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs @@ -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() {