Skip to content

Commit 63b69a2

Browse files
authored
[Blazor] Suppress JSDisconnectedException in IJSObjectReference.DisposeAsync (#64773)
1 parent 870c9d6 commit 63b69a2

File tree

2 files changed

+60
-1
lines changed

2 files changed

+60
-1
lines changed

src/JSInterop/Microsoft.JSInterop/src/Implementation/JSObjectReference.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,15 @@ public async ValueTask DisposeAsync()
104104
{
105105
Disposed = true;
106106

107-
await _jsRuntime.InvokeVoidAsync("DotNet.disposeJSObjectReferenceById", Id);
107+
try
108+
{
109+
await _jsRuntime.InvokeVoidAsync("DotNet.disposeJSObjectReferenceById", Id);
110+
}
111+
catch (JSDisconnectedException)
112+
{
113+
// If the JavaScript runtime is disconnected, there's no need to dispose the JS object reference
114+
// as the JS side is already gone. We can safely ignore this exception.
115+
}
108116
}
109117
}
110118

src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceTest.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,37 @@ public void JSInProcessObjectReference_Dispose_DisallowsFurtherInteropCalls()
6666
Assert.Throws<ObjectDisposedException>(() => jsObject.Invoke<object>("test", "arg1", "arg2"));
6767
}
6868

69+
[Fact]
70+
public async Task JSObjectReference_DisposeAsync_IgnoresJSDisconnectedException()
71+
{
72+
// Arrange
73+
var jsRuntime = new TestJSRuntimeThatThrowsJSDisconnectedException();
74+
var jsObject = new JSObjectReference(jsRuntime, 0);
75+
76+
// Act & Assert - Should not throw
77+
await jsObject.DisposeAsync();
78+
79+
// Verify dispose was attempted
80+
Assert.Equal(1, jsRuntime.BeginInvokeJSInvocationCount);
81+
}
82+
83+
[Fact]
84+
public async Task JSObjectReference_DisposeAsync_IgnoresJSDisconnectedException_OnMultipleCalls()
85+
{
86+
// Arrange
87+
var jsRuntime = new TestJSRuntimeThatThrowsJSDisconnectedException();
88+
var jsObject = new JSObjectReference(jsRuntime, 0);
89+
90+
// Act & Assert - Should not throw on first call
91+
await jsObject.DisposeAsync();
92+
93+
// Act & Assert - Should not throw on second call (no-op)
94+
await jsObject.DisposeAsync();
95+
96+
// Verify dispose was only attempted once
97+
Assert.Equal(1, jsRuntime.BeginInvokeJSInvocationCount);
98+
}
99+
69100
class TestJSRuntime : JSRuntime
70101
{
71102
public int BeginInvokeJSInvocationCount { get; private set; }
@@ -85,6 +116,26 @@ protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocation
85116
}
86117
}
87118

119+
class TestJSRuntimeThatThrowsJSDisconnectedException : JSRuntime
120+
{
121+
public int BeginInvokeJSInvocationCount { get; private set; }
122+
123+
protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo)
124+
{
125+
BeginInvokeJSInvocationCount++;
126+
throw new JSDisconnectedException("JavaScript interop calls cannot be issued at this time. This is because the circuit has disconnected and is being disposed.");
127+
}
128+
129+
protected override void BeginInvokeJS(long taskId, string identifier, [StringSyntax("Json")] string? argsJson, JSCallResultType resultType, long targetInstanceId)
130+
{
131+
throw new NotImplementedException();
132+
}
133+
134+
protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult)
135+
{
136+
}
137+
}
138+
88139
class TestJSInProcessRuntime : JSInProcessRuntime
89140
{
90141
public int InvokeJSInvocationCount { get; private set; }

0 commit comments

Comments
 (0)