JSDisconnectedException in RadzenDataGrid Dispose #328

Closed
opened 2026-01-29 17:35:27 +00:00 by claunia · 4 comments
Owner

Originally created by @dxrdxr on GitHub (Feb 12, 2022).

Describe the bug
I added a NotificationService.Notify to my server side Blazor app and it is now repeatedly throwing JSDisconnectedException when I refresh the page.

The call stack:

Microsoft.AspNetCore.Components.Server.dll!Microsoft.AspNetCore.Components.Server.Circuits.RemoteJSRuntime.BeginInvokeJS(long asyncHandle, string identifier, string argsJson, Microsoft.JSInterop.JSCallResultType resultType, long targetInstanceId) Line 116
	at /_/src/Components/Server/src/Circuits/RemoteJSRuntime.cs(116)
Microsoft.JSInterop.dll!Microsoft.JSInterop.JSRuntime.InvokeAsync<Microsoft.JSInterop.Infrastructure.IJSVoidResult>(long targetInstanceId, string identifier, System.Threading.CancellationToken cancellationToken, object[] args) Line 132
	at /_/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs(132)
Microsoft.JSInterop.dll!Microsoft.JSInterop.JSRuntime.InvokeAsync<Microsoft.JSInterop.Infrastructure.IJSVoidResult>(long targetInstanceId, string identifier, object[] args) Line 91
	at /_/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs(91)
Microsoft.JSInterop.dll!Microsoft.JSInterop.JSRuntime.InvokeAsync<Microsoft.JSInterop.Infrastructure.IJSVoidResult>(string identifier, object[] args) Line 69
	at /_/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs(69)
Microsoft.JSInterop.dll!Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(Microsoft.JSInterop.IJSRuntime jsRuntime, string identifier, object[] args) Line 32
	at /_/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs(32)
[Resuming Async Method]
System.Private.CoreLib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start<Microsoft.JSInterop.JSRuntimeExtensions.<InvokeVoidAsync>d__0>(ref Microsoft.JSInterop.JSRuntimeExtensions.<InvokeVoidAsync>d__0 stateMachine) Line 38
	at /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.cs(38)
Radzen.Blazor.dll!Radzen.Blazor.RadzenDataGrid<DiveShop.ViewModels.CertificationVM>.Dispose()
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Rendering.ComponentState.Dispose() Line 252
	at /_/src/Components/Components/src/Rendering/ComponentState.cs(252)
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.Renderer.Dispose(bool disposing) Line 1030
	at /_/src/Components/Components/src/RenderTree/Renderer.cs(1030)
Microsoft.AspNetCore.Components.Web.dll!Microsoft.AspNetCore.Components.RenderTree.WebRenderer.Dispose(bool disposing) Line 87
	at /_/src/Components/Web/src/WebRenderer.cs(87)
Microsoft.AspNetCore.Components.Server.dll!Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer.Dispose(bool disposing) Line 121
	at /_/src/Components/Server/src/Circuits/RemoteRenderer.cs(121)
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.Renderer.Dispose() Line 1089
	at /_/src/Components/Components/src/RenderTree/Renderer.cs(1089)
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContextDispatcher.InvokeAsync(System.Action workItem) Line 30
	at /_/src/Components/Components/src/Rendering/RendererSynchronizationContextDispatcher.cs(30)
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.Renderer.DisposeAsync() Line 1105
	at /_/src/Components/Components/src/RenderTree/Renderer.cs(1105)
Microsoft.AspNetCore.Components.Server.dll!Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost.DisposeAsync.AnonymousMethod__36_0() Line 188
	at /_/src/Components/Server/src/Circuits/CircuitHost.cs(188)
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.InvokeAsync.AnonymousMethod__9_0(object state) Line 74
	at /_/src/Components/Components/src/Rendering/RendererSynchronizationContext.cs(74)
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.ExecuteSynchronously(System.Threading.Tasks.TaskCompletionSource<object> completion, System.Threading.SendOrPostCallback d, object state) Line 237
[...] uninteresting stacks elided

Environment

  • .Net 6
  • Windows 10
  • Edge (Chromium)
  • Server side Blazor
  • Latest Radzen nuget

Related issues
See the following Blazor issues:

Comments
Based on my reading of the related issues, there is a change in behavior in .Net 6 where calling JSRuntime inside a Dispose method can trigger this exception as the runtime may have disconnected from its circuit. This exception does not exist in .Net 5 so it is likely not caught and handled in the current Radzen library.

Looking at RadzenDataGrid.razor.cs#L1948 this is the likely code that needs to handle this exception.

In looking at the RemoteJSRuntime.cs code where the exception is thrown, one possible way to help avoid this is to check RemoteJSRuntime.IsInitialized which checks the validity of _clientProxy which must not be null. The JSDisconnectedException can only be thrown if _clientProxy is null, therefore if IsInitialized is true then it is safe to call JSRuntime.InvokeVoidAsync.

There may be other places where this same pattern occurs.

Originally created by @dxrdxr on GitHub (Feb 12, 2022). <!-- IMPORTANT: Read this first!!! 1. If you own a Radzen Professional or Еnterprise subscription you can report your issue or ask us a question via email at info@radzen.com. Radzen staff will reply within 24 hours (Professional) or 16 hours (Enterprise) 2. The Radzen staff guarantees a response to issues in this repo only to paid subscribers. 3. If you have a HOW TO question start a new forum thread in the Radzen Community forum: https://forum.radzen.com. Radzen staff will close issues that are HOWTO questions. 4. Please adhere to the issue template. Specify all the steps required to reproduce the issue or link a project which reproduces it easily (without requiring extra steps such as restoring a database). --> **Describe the bug** I added a NotificationService.Notify to my server side Blazor app and it is now repeatedly throwing `JSDisconnectedException` when I refresh the page. The call stack: ``` Microsoft.AspNetCore.Components.Server.dll!Microsoft.AspNetCore.Components.Server.Circuits.RemoteJSRuntime.BeginInvokeJS(long asyncHandle, string identifier, string argsJson, Microsoft.JSInterop.JSCallResultType resultType, long targetInstanceId) Line 116 at /_/src/Components/Server/src/Circuits/RemoteJSRuntime.cs(116) Microsoft.JSInterop.dll!Microsoft.JSInterop.JSRuntime.InvokeAsync<Microsoft.JSInterop.Infrastructure.IJSVoidResult>(long targetInstanceId, string identifier, System.Threading.CancellationToken cancellationToken, object[] args) Line 132 at /_/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs(132) Microsoft.JSInterop.dll!Microsoft.JSInterop.JSRuntime.InvokeAsync<Microsoft.JSInterop.Infrastructure.IJSVoidResult>(long targetInstanceId, string identifier, object[] args) Line 91 at /_/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs(91) Microsoft.JSInterop.dll!Microsoft.JSInterop.JSRuntime.InvokeAsync<Microsoft.JSInterop.Infrastructure.IJSVoidResult>(string identifier, object[] args) Line 69 at /_/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs(69) Microsoft.JSInterop.dll!Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(Microsoft.JSInterop.IJSRuntime jsRuntime, string identifier, object[] args) Line 32 at /_/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs(32) [Resuming Async Method] System.Private.CoreLib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start<Microsoft.JSInterop.JSRuntimeExtensions.<InvokeVoidAsync>d__0>(ref Microsoft.JSInterop.JSRuntimeExtensions.<InvokeVoidAsync>d__0 stateMachine) Line 38 at /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.cs(38) Radzen.Blazor.dll!Radzen.Blazor.RadzenDataGrid<DiveShop.ViewModels.CertificationVM>.Dispose() Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Rendering.ComponentState.Dispose() Line 252 at /_/src/Components/Components/src/Rendering/ComponentState.cs(252) Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.Renderer.Dispose(bool disposing) Line 1030 at /_/src/Components/Components/src/RenderTree/Renderer.cs(1030) Microsoft.AspNetCore.Components.Web.dll!Microsoft.AspNetCore.Components.RenderTree.WebRenderer.Dispose(bool disposing) Line 87 at /_/src/Components/Web/src/WebRenderer.cs(87) Microsoft.AspNetCore.Components.Server.dll!Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer.Dispose(bool disposing) Line 121 at /_/src/Components/Server/src/Circuits/RemoteRenderer.cs(121) Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.Renderer.Dispose() Line 1089 at /_/src/Components/Components/src/RenderTree/Renderer.cs(1089) Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContextDispatcher.InvokeAsync(System.Action workItem) Line 30 at /_/src/Components/Components/src/Rendering/RendererSynchronizationContextDispatcher.cs(30) Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.Renderer.DisposeAsync() Line 1105 at /_/src/Components/Components/src/RenderTree/Renderer.cs(1105) Microsoft.AspNetCore.Components.Server.dll!Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost.DisposeAsync.AnonymousMethod__36_0() Line 188 at /_/src/Components/Server/src/Circuits/CircuitHost.cs(188) Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.InvokeAsync.AnonymousMethod__9_0(object state) Line 74 at /_/src/Components/Components/src/Rendering/RendererSynchronizationContext.cs(74) Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.ExecuteSynchronously(System.Threading.Tasks.TaskCompletionSource<object> completion, System.Threading.SendOrPostCallback d, object state) Line 237 [...] uninteresting stacks elided ``` **Environment** - .Net 6 - Windows 10 - Edge (Chromium) - Server side Blazor - Latest Radzen nuget **Related issues** See the following Blazor issues: - [Blazor 33336](https://github.com/dotnet/aspnetcore/issues/33336) - [Blazor 32901](https://github.com/dotnet/aspnetcore/pull/32901) - [Blazor 32808](https://github.com/dotnet/aspnetcore/issues/32808) **Comments** Based on my reading of the related issues, there is a change in behavior in .Net 6 where calling `JSRuntime` inside a `Dispose` method can trigger this exception as the runtime may have disconnected from its circuit. This exception does not exist in .Net 5 so it is likely not caught and handled in the current Radzen library. Looking at [RadzenDataGrid.razor.cs#L1948](https://github.com/radzenhq/radzen-blazor/blob/c6ce339d40ad4474f069feb6e5a33524b18fd8f3/Radzen.Blazor/RadzenDataGrid.razor.cs#L1948) this is the likely code that needs to handle this exception. In looking at the RemoteJSRuntime.cs code where the exception is thrown, one possible way to help avoid this is to check `RemoteJSRuntime.IsInitialized` which checks the validity of `_clientProxy` which must not be `null`. The `JSDisconnectedException` can only be thrown if `_clientProxy` is null, therefore if `IsInitialized` is true then it is safe to call `JSRuntime.InvokeVoidAsync`. There may be other places where this same pattern occurs.
Author
Owner

@enchev commented on GitHub (Feb 14, 2022):

I'm unable to reproduce such exception on our demos: https://blazor.radzen.com/notification

@enchev commented on GitHub (Feb 14, 2022): I'm unable to reproduce such exception on our demos: https://blazor.radzen.com/notification
Author
Owner

@dxrdxr commented on GitHub (Feb 15, 2022):

It appears that pointing a finger at Notifications was a red herring. I removed notifications and it still throws.

I have distilled my repo down to a very minimal test case that only involves a DataGrid and I can consistently see the exceptions being throw when the Page is refreshed.

See this test repro.

@dxrdxr commented on GitHub (Feb 15, 2022): It appears that pointing a finger at Notifications was a red herring. I removed notifications and it still throws. I have distilled my repo down to a very minimal test case that only involves a DataGrid and I can consistently see the exceptions being throw when the Page is refreshed. See this [test](https://github.com/dxrdxr/RadzenTestCaseDS) repro.
Author
Owner

@enchev commented on GitHub (Feb 15, 2022):

I'm afraid that we cannot do anything else to prevent this. There is already check if the JSRuntime is available. And the StateHasChanged is in try/catch block.

@enchev commented on GitHub (Feb 15, 2022): I'm afraid that we cannot do anything else to prevent this. [There is](https://github.com/radzenhq/radzen-blazor/blob/c6ce339d40ad4474f069feb6e5a33524b18fd8f3/Radzen.Blazor/RadzenDataGrid.razor.cs#L1952) already check [if the JSRuntime is available](https://github.com/dotnet/aspnetcore/issues/38664). And the StateHasChanged is in try/catch block.
Author
Owner

@dxrdxr commented on GitHub (Feb 15, 2022):

A little more debugging on this.

The code is failing in the RadzenDataGrid.Dispose method, the critical code is where it checks IsJSRuntimeAvailable to be true, if so then in calls JSRuntime.InvokeVoidAsync which throws JSDisconnectedException.

If you look at JSRuntime.IsInitialized it is false when the Invoke is called, so it is not unexpected that this fails.

Tracing through my testcase for when IsJSRuntimeAvailable is set and when it is get, you can observe that on initial access to the page it is set to true in OnAfterRenderAsync and a few more times when it is already true. In each case JSRuntime.IsInitialized is true.

The I hit Refresh (^R) I see a call to RadzenDataGrid.Dispose with a different UniqueID than the previous cases and IsJSRuntimeAvailable is false and JSRuntime.IsInitialized is false .

There is then another call to RadzenDataGrid.Dispose with the same UniqueID as the initial access, now IsJSRuntimeAvailable is still true but JSRuntime.IsInitialized is false ! The exception is thus thrown.

My working theory is that when the browser does a Refresh of the page, the JSRuntime is reset (new instance?). But all of the existing RadzenComponents have not had their Dispose methods called. This appears to fit with the discussions in the Issues linked above, that the Dispose method may take up to 60 seconds to be called in some cases.

I don't understand the history of the RadzenComponent code and why it is independently tracking the state of the JSRuntime, and notably that it never tracks the case where the runtime goes away (never set to false). So this appears to be a bad assumption that the Dispose method will be called before the JSRuntime goes away, and with .Net 6.0 things have changed where it does go away first (race?).

Without knowing the internal details, I would opine that the property be removed and the checks such as this:

if (IsJSRuntimeAvailable)
{
...
}             

Be replaced by:

if (JSRuntime?.IsInitialized)
{
...
}      

However, IsInitialized may be Public but it is not documented, so it may not be stable to rely on. The only documented way to know if the runtime is initialized is if OnAfterRenderAsync has been called, but I don't see how to detect if it has been destroyed.

An alternative approach that might work is to grab a reference to a IJSObjectReference in OnAfterRenderAsync and hold on to it, then in the RadzenComponent.Dispose actually dispose of the reference. This will guarantee that it still exists, but it may still not be in a usable state.

Or maybe the only stable thing to do is to simply catch JSDisconnectedException and swallow it? But note that this can only be done in a .Net 6.0 version of Radzen!

@dxrdxr commented on GitHub (Feb 15, 2022): A little more debugging on this. The code is failing in the `RadzenDataGrid.Dispose` method, the critical code is where it checks `IsJSRuntimeAvailable` to be `true`, if so then in calls `JSRuntime.InvokeVoidAsync` which throws `JSDisconnectedException`. If you look at `JSRuntime.IsInitialized` it is `false` when the Invoke is called, so it is not unexpected that this fails. Tracing through my testcase for when `IsJSRuntimeAvailable` is set and when it is get, you can observe that on initial access to the page it is set to `true` in `OnAfterRenderAsync` and a few more times when it is already `true`. In each case `JSRuntime.IsInitialized` is `true`. The I hit Refresh (^R) I see a call to `RadzenDataGrid.Dispose` with a different `UniqueID` than the previous cases and `IsJSRuntimeAvailable` is `false` and `JSRuntime.IsInitialized` is `false` . There is then another call to `RadzenDataGrid.Dispose` with the same `UniqueID` as the initial access, now `IsJSRuntimeAvailable` is still `true` but `JSRuntime.IsInitialized` is `false` ! The exception is thus thrown. My working theory is that when the browser does a Refresh of the page, the `JSRuntime` is reset (new instance?). But all of the existing `RadzenComponents` have not had their `Dispose` methods called. This appears to fit with the discussions in the Issues linked above, that the `Dispose` method may take up to 60 seconds to be called in some cases. I don't understand the history of the `RadzenComponent` code and why it is independently tracking the state of the `JSRuntime`, and notably that it never tracks the case where the runtime goes away (never set to `false`). So this appears to be a bad assumption that the `Dispose` method will be called before the `JSRuntime` goes away, and with .Net 6.0 things have changed where it does go away first (race?). Without knowing the internal details, I would opine that the property be removed and the checks such as this: ``` if (IsJSRuntimeAvailable) { ... } ``` Be replaced by: ``` if (JSRuntime?.IsInitialized) { ... } ``` However, `IsInitialized` may be `Public` but it is not documented, so it may not be stable to rely on. The only documented way to know [if the runtime is initialized](https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-6.0#detect-when-a-blazor-server-app-is-prerendering) is if `OnAfterRenderAsync` has been called, but I don't see how to detect if it has been destroyed. An alternative approach that might work is to grab a reference to a `IJSObjectReference` in `OnAfterRenderAsync` and hold on to it, then in the `RadzenComponent.Dispose` actually dispose of the reference. This will guarantee that it still exists, but it may still not be in a usable state. Or maybe the only stable thing to do is to simply catch `JSDisconnectedException` and swallow it? **But** note that this can only be done in a .Net 6.0 version of Radzen!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/radzen-blazor#328