#4746·SignalR

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:

...but in this case, the cause is when:

  1. The thread enters Connection.Start(IClientTransport), it then flows through to ChangeState(Disconnected,Connecting).
  2. The (external) event-handler for ChangeState throws an exception when NewState == Connecting.
    • ...so Connection.Start never gets to set _disconnectCts while within lock(_startLock).
  3. ...so the implicit .Dispose call (due to using) enters Connection.Disconnect, which assumes _disconnectCts is non-null.

Repro steps:


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:

Image