3

I have a SignalR HubConnection within my Blazor WebAssembly application and whilst it works most of the time, if I reload the page (via the browser reload) then I often am getting the following error in the console and the connection is not made:

Uncaught Error: The delegate target that is being invoked is no longer available. Please check if it has been prematurely GC'd. at Object.invoke_delegate (dotnet.5.0.4.js:1) at WebSocket. (dotnet.5.0.4.js:1)

Here's a rough, simplified view of the code where I create the HubConnection (and dispose it).

@inherits LayoutBase
@attribute [Authorize]

<AuthorizeView>
    <Authorized>
        //...
    </Authorized>
    <NotAuthorized>
        //...
    </NotAuthorized>
</AuthorizeView>
public class LayoutBase : LayoutComponentBase, IAsyncDisposable
{
    [Inject] public IAccessTokenProvider AccessTokenProvider { get; set; }
    
    private readonly HubConnection _hubConnection;
    
    protected override async Task OnInitializedAsync()
    {
        _hubConnection = new HubConnectionBuilder()
                    .AddNewtonsoftJsonProtocol(c =>
                    {
                        //...
                    })
                    .WithUrl(notificationHubUrl, option => option.AccessTokenProvider = GetAccessToken)
                    .WithAutomaticReconnect()
                    .Build();                   
                    
        _hubConnection.Closed += HubConnectionOnClosed;
        _hubConnection.Reconnected += HubConnectionOnReconnected;
        _hubConnection.Reconnecting += HubConnectionOnReconnecting;
        
        await _hubConnection.StartAsync()
        await base.OnInitializedAsync();
    }   
    
    private async Task<string> GetAccessToken()
    {
        var tokenResult = await AccessTokenProvider.RequestAccessToken(...)
        // etc...
    }
    
    // .. Event Handlers
    
    public ValueTask DisposeAsync()
    {
        _logger.LogInformation($"Disposing Hub: {_hubConnection.ConnectionId}");

        _hubConnection.Closed -= HubConnectionOnClosed;
        _hubConnection.Reconnected -= HubConnectionOnReconnected;
        _hubConnection.Reconnecting -= HubConnectionOnReconnecting;

        return _hubConnection.DisposeAsync();
    }
}

Previously I had it as an injected service but I eventually simplified it to this structure but it continues to get this error on reload. It's not every time I reload but most times.

I have tried changing the dispose pattern without success. I can't find any information on the error anywhere else.

Any ideas?

oatsoda
  • 2,088
  • 2
  • 26
  • 49
  • Have you tried using the non-`async` implementation of `IDisposable`? You keyword *sometimes* implies it's not being disposed properly. – Cory Podojil Mar 19 '21 at 14:39
  • Yeah, I have -though the `HubConnection` itself only has an async dispose... – oatsoda Mar 19 '21 at 15:06
  • I don't have the answer to the question, but I am currently experiencing the same issue (and still investigating). Have you tried to remove other parts of your code (other than what is for the SignalR connection) ? During my investigations I've found that it sometimes reduced the frequency of this issue, although it is for me something that's intermittent. – user3918555 Apr 19 '21 at 12:20
  • @user3918555 I've tried mucking about with scopes but no luck. It only occasionally happens, but I have noticed that it usually logs the error twice. I'm wondering if it might be caused by the OpenId auth flow causing multiple, overlapping, loads of my Blazor Wasm app.. – oatsoda Apr 28 '21 at 09:38

1 Answers1

0

I don't have a definitive answer as to the underlying reason but I suspect that this is a bug somewhere in the SignalR/dotnet framework resulting in the GCing of a delegate because something drops a reference to it.

One way I've managed to provoke this error reasonably consistently is to have a handler returning just a Task, e.g.

_hubConnection.On<TEvent>(eventType.Name, OnEvent);

where OnEvent looks like this:

// THIS IS THE BROKEN SIGNATURE - DO NOT USE
private async Task OnEvent<TEvent>(TEvent e)
{
}

A workaround which appears to have fixed it for me is to make the handler actually return something. This seems to make something deeper in the framework hold a reference for longer so that it doesn't get GC'ed. E.g.

// WORKS ON MY MACHINE - Note the return type of Task<object>
private async Task<object> OnEvent<TEvent>(TEvent e)
{
  // ... Do stuff
  return null;
}
uglybugger
  • 905
  • 9
  • 7