NullReferenceException in Connection.Disconnect when disposing HubConnection after throwing from within a StateChanged event-handler
Author: daiplusplusCreated Sep 4, 2025Updated Sep 4, 2025
This is similar, but not identical, to previously reported NREs in Connection.Stop, such as:
- https://github.com/SignalR/SignalR/issues/4458
- https://github.com/SignalR/SignalR/issues/4519
- https://github.com/SignalR/SignalR/issues/4403
...but in this case, the cause is when:
- The thread enters
Connection.Start(IClientTransport), it then flows through toChangeState(Disconnected,Connecting). - The (external) event-handler for
ChangeStatethrows an exception whenNewState == Connecting.- ...so
Connection.Startnever gets to set_disconnectCtswhile withinlock(_startLock).
- ...so
- ...so the implicit
.Disposecall (due tousing) entersConnection.Disconnect, which assumes_disconnectCtsis non-null.
Repro steps:
- This program consistently produces the
NullReferenceExceptionwhen run in LinqPad 5 (for .NET Framework 4.8). - This is using
Microsoft.AspNet.SignalR.Clientversion 2.4.3, which is the current release as of 2025-09-03. - On a related note, I'd like to help further, but I'm unable to build the SignalR library myself because the SignalR project build scripts don't work anymore
- Because the NRE is a second-order exception, the original exception (
InvalidOperationExceptionbelow) is not exposed to the caller or host program and gets lost. - (The
HubConnection3subclass exists only to allow setting a breakpoint withinConnection.Dispose).
public static async Task Main()
{
using( HubConnection3 hc = new HubConnection3( url: "https://etc/signalr/hubs/etc" ) )
{
hc.StateChanged += OnStateChange;
Task startTask = StartHCAsync( hc );
Task delayTask = Task.Delay( TimeSpan.FromHours( 1 ) );
Task first = await Task.WhenAny( startTask, delayTask ).ConfigureAwait(false);
if( first == startTask && first.Status != TaskStatus.RanToCompletion )
{
await first.ConfigureAwait(false);
}
await Task.WhenAll( startTask, delayTask ).ConfigureAwait(false);
}
}
private static void OnStateChange( StateChange delta )
{
switch( delta.NewState )
{
case Microsoft.AspNet.SignalR.Client.ConnectionState.Connecting:
case Microsoft.AspNet.SignalR.Client.ConnectionState.Reconnecting:
throw new InvalidOperationException( "boo!" );
default:
return;
}
}
private static async Task StartHCAsync( HubConnection hc )
{
await hc.Start().ConfigureAwait(false);
}
class HubConnection3 : HubConnection
{
public HubConnection3( String url ) : base( url )
{
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
}
}Screenshot proof:
Source: SignalR/SignalR