Skip to content

Commit 941f88e

Browse files
Copilotilonatommy
andcommitted
Implement JSTimeoutException and timeout handling in ComponentBase
Co-authored-by: ilonatommy <[email protected]>
1 parent e8c7fb3 commit 941f88e

File tree

6 files changed

+73
-4
lines changed

6 files changed

+73
-4
lines changed

src/Components/Components/src/ComponentBase.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.Components.Rendering;
5+
using Microsoft.JSInterop;
56

67
namespace Microsoft.AspNetCore.Components;
78

@@ -289,6 +290,11 @@ private async Task RunInitAndSetParametersAsync()
289290
{
290291
await task;
291292
}
293+
catch (JSTimeoutException)
294+
{
295+
// Let JSTimeoutException bubble up to provide meaningful error information
296+
throw;
297+
}
292298
catch // avoiding exception filters for AOT runtime support
293299
{
294300
// Ignore exceptions from task cancellations.
@@ -332,6 +338,11 @@ private async Task CallStateHasChangedOnAsyncCompletion(Task task)
332338
{
333339
await task;
334340
}
341+
catch (JSTimeoutException)
342+
{
343+
// Let JSTimeoutException bubble up to provide meaningful error information
344+
throw;
345+
}
335346
catch // avoiding exception filters for AOT runtime support
336347
{
337348
// Ignore exceptions from task cancellations, but don't bother issuing a state change.

src/Components/Components/test/ComponentBaseTest.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Diagnostics;
55
using Microsoft.AspNetCore.Components.Rendering;
66
using Microsoft.AspNetCore.Components.Test.Helpers;
7+
using Microsoft.JSInterop;
78

89
namespace Microsoft.AspNetCore.Components.Test;
910

@@ -463,6 +464,24 @@ public async Task RenderRootComponentAsync_ReportsErrorDuringOnParameterSetAsync
463464
Assert.Same(expected, actual);
464465
}
465466

467+
[Fact]
468+
public async Task ComponentBase_AllowsJSTimeoutExceptionToBubbleUp()
469+
{
470+
// Arrange
471+
var renderer = new TestRenderer();
472+
var component = new TestComponent();
473+
474+
var timeoutException = new JSTimeoutException("Test timeout");
475+
component.OnParametersSetAsyncLogic = _ => Task.FromException(timeoutException);
476+
477+
// Act & Assert
478+
var componentId = renderer.AssignRootComponentId(component);
479+
var actual = await Assert.ThrowsAsync<JSTimeoutException>(() => renderer.RenderRootComponentAsync(componentId));
480+
481+
// Assert
482+
Assert.Same(timeoutException, actual);
483+
}
484+
466485
private class TestComponent : ComponentBase
467486
{
468487
public bool RunsBaseOnInit { get; set; } = true;

src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,17 @@ public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, Cancellat
113113
if (DefaultAsyncTimeout.HasValue)
114114
{
115115
using var cts = new CancellationTokenSource(DefaultAsyncTimeout.Value);
116-
// We need to await here due to the using
117-
return await InvokeAsync<TValue>(targetInstanceId, identifier, callType, cts.Token, args);
116+
117+
try
118+
{
119+
// We need to await here due to the using
120+
return await InvokeAsync<TValue>(targetInstanceId, identifier, callType, cts.Token, args);
121+
}
122+
catch (OperationCanceledException) when (cts.Token.IsCancellationRequested)
123+
{
124+
// This was cancelled due to our timeout, throw a more meaningful exception
125+
throw new JSTimeoutException("A JavaScript interop call timed out. Consider increasing the timeout duration if the operation is expected to take longer.");
126+
}
118127
}
119128

120129
return await InvokeAsync<TValue>(targetInstanceId, identifier, callType, CancellationToken.None, args);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.JSInterop;
5+
6+
/// <summary>
7+
/// Represents errors that occur when a JavaScript interop call times out.
8+
/// </summary>
9+
public class JSTimeoutException : JSException
10+
{
11+
/// <summary>
12+
/// Constructs an instance of <see cref="JSTimeoutException"/>.
13+
/// </summary>
14+
/// <param name="message">The exception message.</param>
15+
public JSTimeoutException(string message) : base(message)
16+
{
17+
}
18+
19+
/// <summary>
20+
/// Constructs an instance of <see cref="JSTimeoutException"/>.
21+
/// </summary>
22+
/// <param name="message">The exception message.</param>
23+
/// <param name="innerException">The inner exception.</param>
24+
public JSTimeoutException(string message, Exception innerException) : base(message, innerException)
25+
{
26+
}
27+
}

src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, object?[]? args
5454
Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask<Microsoft.JSInterop.IJSObjectReference!>
5555
Microsoft.JSInterop.JSRuntime.SetValueAsync<TValue>(string! identifier, TValue value) -> System.Threading.Tasks.ValueTask
5656
Microsoft.JSInterop.JSRuntime.SetValueAsync<TValue>(string! identifier, TValue value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask
57+
Microsoft.JSInterop.JSTimeoutException
58+
Microsoft.JSInterop.JSTimeoutException.JSTimeoutException(string! message) -> void
59+
Microsoft.JSInterop.JSTimeoutException.JSTimeoutException(string! message, System.Exception! innerException) -> void
5760
static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, params object?[]? args) -> System.Threading.Tasks.ValueTask<Microsoft.JSInterop.IJSObjectReference!>
5861
static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask<Microsoft.JSInterop.IJSObjectReference!>
5962
static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.TimeSpan timeout, object?[]? args) -> System.Threading.Tasks.ValueTask<Microsoft.JSInterop.IJSObjectReference!>

src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public void DispatchesAsyncCallsWithDistinctAsyncHandles()
3737
}
3838

3939
[Fact]
40-
public async Task InvokeAsync_CancelsAsyncTask_AfterDefaultTimeout()
40+
public async Task InvokeAsync_ThrowsJSTimeoutException_AfterDefaultTimeout()
4141
{
4242
// Arrange
4343
var runtime = new TestJSRuntime();
@@ -47,7 +47,7 @@ public async Task InvokeAsync_CancelsAsyncTask_AfterDefaultTimeout()
4747
var task = runtime.InvokeAsync<object>("test identifier 1", "arg1", 123, true);
4848

4949
// Assert
50-
await Assert.ThrowsAsync<TaskCanceledException>(async () => await task);
50+
await Assert.ThrowsAsync<JSTimeoutException>(async () => await task);
5151
}
5252

5353
[Fact]

0 commit comments

Comments
 (0)