@@ -36,99 +36,146 @@ public SignalRExceptionFilter(ILogger<SignalRExceptionFilter> logger,
3636
3737 try
3838 {
39- invocationId = TryGetInvocationId < IHubArgument > ( invocationContext ) ;
39+ invocationId = TryGetInvocationId ( invocationContext ) ;
4040 return await next ( invocationContext ) ;
4141 }
4242 catch ( DbUpdateConcurrencyException )
4343 {
4444 var exception = new ConflictException ( ExceptionMessages . ConcurrencyMessage . ConvertCase ( _convention ) ) ;
45- return await HandleApiExceptionAsync ( invocationContext , exception , invocationId ) ;
45+ await HandleApiExceptionAsync ( invocationContext , exception , invocationId ) ;
46+ return NullResultFor ( invocationContext ) ;
4647 }
4748 catch ( GridifyException ex )
4849 {
4950 var exception = new BadRequestException ( ex . Message . ConvertCase ( _convention ) ) ;
50- return await HandleApiExceptionAsync ( invocationContext , exception , invocationId ) ;
51+ await HandleApiExceptionAsync ( invocationContext , exception , invocationId ) ;
52+ return NullResultFor ( invocationContext ) ;
5153 }
5254 catch ( ApiException ex )
5355 {
54- return await HandleApiExceptionAsync ( invocationContext , ex , invocationId ) ;
56+ await HandleApiExceptionAsync ( invocationContext , ex , invocationId ) ;
57+ return NullResultFor ( invocationContext ) ;
5558 }
5659 catch ( Exception ex )
5760 {
58- return await HandleGeneralExceptionAsync ( invocationContext , ex , invocationId ) ;
61+ await HandleGeneralExceptionAsync ( invocationContext , ex , invocationId ) ;
62+ return NullResultFor ( invocationContext ) ;
5963 }
6064 }
6165
62- private static string TryGetInvocationId < T > ( HubInvocationContext hubInvocationContext ) where T : IHubArgument
66+ private static string TryGetInvocationId ( HubInvocationContext ctx )
6367 {
64- if ( hubInvocationContext . HubMethodArguments is not [ T hubArgument ] )
68+ // 1) Legacy behavior: exactly one arg that implements IHubArgument
69+ if ( ctx . HubMethodArguments is [ IHubArgument single ] &&
70+ ! string . IsNullOrWhiteSpace ( single . InvocationId ) )
6571 {
66- throw new BadRequestException ( "Invalid hub method arguments. Request model does not implement IHubArgument interface." ) ;
72+ return single . InvocationId ;
6773 }
6874
69- var invocationId = hubArgument . InvocationId ;
70- if ( string . IsNullOrWhiteSpace ( invocationId ) )
75+ // 2) New tolerant path: scan all args for first valid IHubArgument
76+ foreach ( var arg in ctx . HubMethodArguments )
7177 {
72- throw new BadRequestException ( "Invocation ID cannot be null, empty, or whitespace." ) ;
78+ if ( arg is IHubArgument ha && ! string . IsNullOrWhiteSpace ( ha . InvocationId ) )
79+ return ha . InvocationId ;
7380 }
7481
75- return invocationId ;
82+ // 3) Optional non-breaking fallback: header/query (safe to keep off if you don’t want it)
83+ var http = ctx . Context . GetHttpContext ( ) ;
84+ var id = http ? . Request
85+ . Headers [ "x-invocation-id" ]
86+ . FirstOrDefault ( )
87+ ?? http ? . Request
88+ . Query [ "invocation_id" ]
89+ . FirstOrDefault ( ) ;
90+
91+ return ! string . IsNullOrWhiteSpace ( id )
92+ ? id
93+ : throw new BadRequestException ( "Invocation ID cannot be null, empty, or whitespace." ) ;
7694 }
7795
78- private async Task < HubErrorResponse > HandleApiExceptionAsync ( HubInvocationContext invocationContext ,
79- ApiException exception ,
96+ private async Task HandleApiExceptionAsync ( HubInvocationContext ctx ,
97+ ApiException ex ,
8098 string invocationId )
8199 {
82- var response = new HubErrorResponse
100+ var traceId = Activity . Current ? . TraceId . ToString ( ) ?? string . Empty ;
101+
102+ using ( _logger . BeginScope ( new Dictionary < string , object >
103+ {
104+ [ "trace_id" ] = traceId ,
105+ [ "hub" ] = ctx . Hub . GetType ( )
106+ . Name ,
107+ [ "method" ] = ctx . HubMethodName ,
108+ [ "connection_id" ] = ctx . Context . ConnectionId ,
109+ [ "user_id" ] = ctx . Context . UserIdentifier ?? "" ,
110+ [ "invocation_id" ] = invocationId ,
111+ [ "status_code" ] = ex . StatusCode
112+ } ) )
83113 {
84- TraceId = Activity . Current ? . RootId ?? "" ,
85- InvocationId = invocationId ,
86- Instance = invocationContext . HubMethodName ,
87- StatusCode = exception . StatusCode ,
88- Message = exception . Message . ConvertCase ( _convention ) ,
89- Errors = exception . Errors
90- } ;
91-
92- if ( response . Errors is null || response . Errors . Count == 0 )
93- {
94- _logger . LogWarning ( "SignalR Exception Encountered: {Message}" , response . Message ) ;
95- }
96- else
97- {
98- _logger . LogWarning ( "SignalR Exception Encountered: {Message} with errors: {@Errors}" ,
99- response . Message ,
100- response . Errors ) ;
114+ var response = new HubErrorResponse
115+ {
116+ TraceId = traceId ,
117+ InvocationId = invocationId ,
118+ Instance = ctx . HubMethodName ,
119+ StatusCode = ex . StatusCode ,
120+ Message = ex . Message . ConvertCase ( _convention ) ,
121+ Errors = ex . Errors . ConvertCase ( _convention )
122+ } ;
123+
124+ if ( response . Errors is null || response . Errors . Count == 0 )
125+ {
126+ _logger . LogWarning ( "SignalR exception: {Message}" , response . Message ) ;
127+ }
128+ else
129+ {
130+ _logger . LogWarning ( "SignalR exception: {Message} with errors: {@Errors}" ,
131+ response . Message ,
132+ response . Errors ) ;
133+ }
134+
135+ await ctx . Hub . Clients . Caller . SendAsync ( "ReceiveError" , response , ctx . Context . ConnectionAborted ) ;
101136 }
102-
103- await invocationContext . Hub . Clients . Caller . SendAsync ( "ReceiveError" , response ) ;
104-
105- return response ;
106137 }
107138
108- private async Task < HubErrorResponse > HandleGeneralExceptionAsync ( HubInvocationContext invocationContext ,
109- Exception exception ,
139+ private async Task HandleGeneralExceptionAsync ( HubInvocationContext ctx ,
140+ Exception ex ,
110141 string invocationId )
111142 {
112- var verboseMessage = exception . CreateVerboseExceptionMessage ( ) ;
113-
114- var response = new HubErrorResponse
143+ var traceId = Activity . Current ? . TraceId . ToString ( ) ?? string . Empty ;
144+ var verbose = ex . CreateVerboseExceptionMessage ( ) ;
145+
146+ using ( _logger . BeginScope ( new Dictionary < string , object >
147+ {
148+ [ "trace_id" ] = traceId ,
149+ [ "hub" ] = ctx . Hub . GetType ( )
150+ . Name ,
151+ [ "method" ] = ctx . HubMethodName ,
152+ [ "connection_id" ] = ctx . Context . ConnectionId ,
153+ [ "user_id" ] = ctx . Context . UserIdentifier ?? "" ,
154+ [ "invocation_id" ] = invocationId ,
155+ [ "status_code" ] = 500
156+ } ) )
115157 {
116- TraceId = Activity . Current ? . RootId ?? "" ,
117- InvocationId = invocationId ,
118- Instance = invocationContext . HubMethodName ,
119- StatusCode = 500 ,
120- Message = ExceptionMessages . DefaultMessage . ConvertCase ( _convention )
121- } ;
122-
123- if ( _visibility == "Private" )
124- {
125- response . Message = verboseMessage . ConvertCase ( _convention ) ;
158+ var response = new HubErrorResponse
159+ {
160+ TraceId = traceId ,
161+ InvocationId = invocationId ,
162+ Instance = ctx . HubMethodName ,
163+ StatusCode = 500 ,
164+ Message = _visibility == "Private"
165+ ? verbose . ConvertCase ( _convention )
166+ : ExceptionMessages . DefaultMessage . ConvertCase ( _convention )
167+ } ;
168+
169+ _logger . LogError ( "Unhandled SignalR exception: {Message}" , verbose ) ;
170+
171+ await ctx . Hub . Clients . Caller . SendAsync ( "ReceiveError" , response , ctx . Context . ConnectionAborted ) ;
126172 }
173+ }
127174
128- _logger . LogError ( "Unhandled exception encountered: {Message}" , verboseMessage ) ;
129-
130- await invocationContext . Hub . Clients . Caller . SendAsync ( "ReceiveError" , response ) ;
131-
132- return response ;
175+ private static object ? NullResultFor ( HubInvocationContext _ )
176+ {
177+ // For Task/void: no payload is emitted.
178+ // For Task<T>/T: caller receives null once (plus ReceiveError event).
179+ return null ;
133180 }
134181}
0 commit comments