Skip to content

Commit 2e8424e

Browse files
authored
Merge pull request #62 from PandaTechAM/development
Total refactoring for performance and documented as well for details
2 parents 2f835e0 + 4c56fbd commit 2e8424e

File tree

11 files changed

+576
-285
lines changed

11 files changed

+576
-285
lines changed

Readme.md

Lines changed: 300 additions & 151 deletions
Large diffs are not rendered by default.

src/ResponseCrafter/ExceptionHandlers/Http/ApiExceptionHandler.cs

Lines changed: 90 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System.Diagnostics;
2+
using System.Globalization;
3+
using System.Text.Json;
24
using FluentImporter.Exceptions;
35
using Gridify;
46
using GridifyExtensions.Exceptions;
@@ -95,19 +97,41 @@ private async Task HandleImportExceptionAsync(HttpContext httpContext,
9597

9698
private async Task HandleBadHttpRequestExceptionAsync(HttpContext httpContext,
9799
BadHttpRequestException badHttpRequestException,
98-
CancellationToken cancellationToken)
100+
CancellationToken ct)
99101
{
100-
if (badHttpRequestException.InnerException is System.Text.Json.JsonException jsonEx
101-
&& jsonEx.Message.ToLower().Contains("missing required properties including"))
102-
{
103-
var exception = new BadRequestException(jsonEx.Message);
104-
await HandleApiExceptionAsync(httpContext, exception, cancellationToken);
105-
}
106-
else
102+
if (badHttpRequestException.InnerException is JsonException je)
107103
{
108-
var exception = new BadRequestException("Bad request. Possibly malformed JSON");
109-
await HandleApiExceptionAsync(httpContext, exception, cancellationToken);
104+
var errors = new Dictionary<string, string>
105+
{
106+
["path"] = (je.Path ?? "$").ConvertCase(_convention),
107+
["detail"] = "json_deserialization_failed".ConvertCase(_convention)
108+
};
109+
110+
var posObj = typeof(JsonException).GetProperty(nameof(JsonException.BytePositionInLine))
111+
?.GetValue(je);
112+
if (posObj is long pos and >= 0)
113+
{
114+
errors["byte_position"] = pos.ToString(CultureInfo.InvariantCulture);
115+
}
116+
117+
var lineObj = typeof(JsonException).GetProperty(nameof(JsonException.LineNumber))
118+
?.GetValue(je);
119+
if (lineObj is long line and >= 0)
120+
{
121+
errors["line_number"] = line.ToString(CultureInfo.InvariantCulture);
122+
}
123+
124+
var ex = new BadRequestException("invalid_json_payload")
125+
{
126+
Errors = errors
127+
};
128+
129+
await HandleApiExceptionAsync(httpContext, ex, ct);
130+
return;
110131
}
132+
133+
var generic = new BadRequestException("bad_request_possibly_malformed_json");
134+
await HandleApiExceptionAsync(httpContext, generic, ct);
111135
}
112136

113137
private async Task HandleGridifyExceptionAsync(HttpContext httpContext,
@@ -117,7 +141,7 @@ private async Task HandleGridifyExceptionAsync(HttpContext httpContext,
117141
var exception = new BadRequestException(gridifyException.Message.ConvertCase(_convention));
118142
await HandleApiExceptionAsync(httpContext, exception, cancellationToken);
119143
}
120-
144+
121145
private async Task HandleGridifyExceptionMapperAsync(HttpContext httpContext,
122146
GridifyMapperException gridifyMapperException,
123147
CancellationToken cancellationToken)
@@ -128,46 +152,62 @@ private async Task HandleGridifyExceptionMapperAsync(HttpContext httpContext,
128152

129153
private async Task HandleApiExceptionAsync(HttpContext httpContext,
130154
ApiException exception,
131-
CancellationToken cancellationToken)
155+
CancellationToken ct)
132156
{
133-
var response = new ErrorResponse
157+
var traceId = Activity.Current?.TraceId.ToString() ?? string.Empty;
158+
var instance = CreateRequestPath(httpContext);
159+
160+
using (_logger.BeginScope(new Dictionary<string, object>
161+
{
162+
["trace_id"] = traceId,
163+
["request_id"] = httpContext.TraceIdentifier,
164+
["instance"] = instance,
165+
["http_method"] = httpContext.Request.Method,
166+
["path"] = httpContext.Request.Path.ToString(),
167+
["status_code"] = exception.StatusCode
168+
}))
134169
{
135-
RequestId = httpContext.TraceIdentifier,
136-
TraceId = Activity.Current?.RootId ?? "",
137-
Instance = CreateRequestPath(httpContext),
138-
StatusCode = exception.StatusCode,
139-
Type = exception.GetType()
140-
.Name,
141-
Errors = exception.Errors,
142-
Message = exception.Message.ConvertCase(_convention)
143-
};
144-
145-
httpContext.Response.StatusCode = exception.StatusCode;
146-
await httpContext.Response.WriteAsJsonAsync(response, cancellationToken);
147-
148-
if (response.Errors is null || response.Errors.Count == 0)
149-
{
150-
_logger.LogWarning("ApiException encountered: {Message}", response.Message);
151-
}
152-
else
153-
{
154-
_logger.LogWarning("ApiException encountered: {Message} with errors: {@Errors}",
155-
response.Message,
156-
response.Errors);
170+
var response = new ErrorResponse
171+
{
172+
RequestId = httpContext.TraceIdentifier,
173+
TraceId = traceId,
174+
Instance = instance,
175+
StatusCode = exception.StatusCode,
176+
Type = exception.GetType()
177+
.Name,
178+
Errors = exception.Errors.ConvertCase(_convention),
179+
Message = exception.Message.ConvertCase(_convention)
180+
};
181+
182+
httpContext.Response.StatusCode = exception.StatusCode;
183+
await httpContext.Response.WriteAsJsonAsync(response, ct);
184+
185+
if (response.Errors is null || response.Errors.Count == 0)
186+
{
187+
_logger.LogWarning("ApiException encountered: {Message}", response.Message);
188+
}
189+
else
190+
{
191+
_logger.LogWarning("ApiException encountered: {Message} with errors: {@Errors}",
192+
response.Message,
193+
response.Errors);
194+
}
157195
}
158196
}
159197

160198
private async Task HandleGeneralExceptionAsync(HttpContext httpContext,
161199
Exception exception,
162200
CancellationToken cancellationToken)
163201
{
202+
var traceId = Activity.Current?.TraceId.ToString() ?? string.Empty;
203+
var instance = CreateRequestPath(httpContext);
164204
var verboseMessage = exception.CreateVerboseExceptionMessage();
165205

166206
var response = new ErrorResponse
167207
{
168208
RequestId = httpContext.TraceIdentifier,
169-
TraceId = Activity.Current?.RootId ?? "",
170-
Instance = CreateRequestPath(httpContext),
209+
TraceId = traceId,
210+
Instance = instance,
171211
StatusCode = 500,
172212
Type = "InternalServerError",
173213
Message = ExceptionMessages.DefaultMessage.ConvertCase(_convention)
@@ -181,9 +221,19 @@ private async Task HandleGeneralExceptionAsync(HttpContext httpContext,
181221
}
182222

183223
httpContext.Response.StatusCode = response.StatusCode;
184-
await httpContext.Response.WriteAsJsonAsync(response, cancellationToken);
185-
186224

187-
_logger.LogError("Unhandled exception encountered: {Message}", verboseMessage);
225+
using (_logger.BeginScope(new Dictionary<string, object> // <-- scope before write (optional)
226+
{
227+
["trace_id"] = traceId,
228+
["request_id"] = httpContext.TraceIdentifier,
229+
["instance"] = instance,
230+
["http_method"] = httpContext.Request.Method,
231+
["path"] = httpContext.Request.Path.ToString(),
232+
["status_code"] = 500
233+
}))
234+
{
235+
await httpContext.Response.WriteAsJsonAsync(response, cancellationToken);
236+
_logger.LogError("Unhandled exception encountered: {Message}", verboseMessage);
237+
}
188238
}
189239
}

src/ResponseCrafter/ExceptionHandlers/SignalR/SignalRExceptionFilter.cs

Lines changed: 103 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)