Skip to content

Commit 3b2dc85

Browse files
niemyjskiCopilot
andcommitted
fix(aspnetcore): add per-request de-dup and typed catch for reflection
- Add HttpContext.Items marker in ExceptionlessExceptionHandler to prevent duplicate event submission when DiagnosticListener also captures the same exception for a request. - Replace generic catch with typed exception catches (TargetInvocation, AmbiguousMatch, Target, MethodAccess, Argument) in GetPropertyValue. - Add tests for de-dup behavior (same exception skipped, different exception still submitted). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent cb1caca commit 3b2dc85

3 files changed

Lines changed: 56 additions & 1 deletion

File tree

src/Platforms/Exceptionless.AspNetCore/ExceptionlessDiagnosticListener.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ private void SubmitException(object payload, string submissionMethod, bool isUnh
5858
if (httpContext == null || exception == null)
5959
return;
6060

61+
// Skip if this exception was already submitted for this request by ExceptionlessExceptionHandler.
62+
if (httpContext.Items.TryGetValue(ExceptionlessExceptionHandler.HttpContextSubmittedKey, out var submitted)
63+
&& ReferenceEquals(submitted, exception))
64+
return;
65+
6166
var contextData = new ContextData();
6267
if (isUnhandledError)
6368
contextData.MarkAsUnhandledError();
@@ -74,7 +79,7 @@ private static object GetPropertyValue(object payload, string propertyName) {
7479
return payload.GetType()
7580
.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.IgnoreCase)?
7681
.GetValue(payload);
77-
} catch {
82+
} catch (Exception ex) when (ex is TargetInvocationException or AmbiguousMatchException or TargetException or MethodAccessException or ArgumentException) {
7883
return null;
7984
}
8085
}

src/Platforms/Exceptionless.AspNetCore/ExceptionlessExceptionHandler.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ public ExceptionlessExceptionHandler(ExceptionlessClient client) {
1313
_client = client ?? ExceptionlessClient.Default;
1414
}
1515

16+
/// <summary>
17+
/// Key used in <see cref="HttpContext.Items"/> to track the exception already submitted for this request,
18+
/// preventing duplicate submissions from the diagnostic listener.
19+
/// </summary>
20+
public const string HttpContextSubmittedKey = "Exceptionless:ExceptionSubmitted";
21+
1622
public ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) {
1723
if (cancellationToken.IsCancellationRequested)
1824
return ValueTask.FromResult(false);
@@ -22,6 +28,7 @@ public ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception excepti
2228
contextData.SetSubmissionMethod(nameof(ExceptionlessExceptionHandler));
2329

2430
exception.ToExceptionless(contextData, _client).SetHttpContext(httpContext).Submit();
31+
httpContext.Items[HttpContextSubmittedKey] = exception;
2532

2633
return ValueTask.FromResult(false);
2734
}

test/Exceptionless.Tests/Platforms/AspNetCoreExceptionCaptureTests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,49 @@ public async Task Invoke_WhenNextDelegateThrows_RethrowsExceptionWithoutSubmitti
137137
Assert.Empty(submittingEvents);
138138
}
139139

140+
[Fact]
141+
public async Task DiagnosticListener_WhenExceptionAlreadySubmittedByHandler_SkipsDuplicateSubmission() {
142+
// Arrange
143+
var submittingEvents = new List<EventSubmittingEventArgs>();
144+
var client = CreateClient(submittingEvents);
145+
var context = CreateHttpContext();
146+
var exception = new InvalidOperationException("unhandled");
147+
var handler = new ExceptionlessExceptionHandler(client);
148+
var listener = new ExceptionlessDiagnosticListener(client);
149+
150+
// Act — handler submits first, then diagnostic listener sees the same exception
151+
await handler.TryHandleAsync(context, exception, CancellationToken.None);
152+
listener.OnNext(new KeyValuePair<string, object>("Microsoft.AspNetCore.Hosting.UnhandledException", new {
153+
httpContext = context,
154+
exception
155+
}));
156+
157+
// Assert — only one submission
158+
Assert.Single(submittingEvents);
159+
}
160+
161+
[Fact]
162+
public void DiagnosticListener_WhenExceptionDiffersFromSubmitted_SubmitsNewEvent() {
163+
// Arrange
164+
var submittingEvents = new List<EventSubmittingEventArgs>();
165+
var client = CreateClient(submittingEvents);
166+
var context = CreateHttpContext();
167+
var listener = new ExceptionlessDiagnosticListener(client);
168+
169+
// Mark a different exception as already submitted
170+
context.Items[ExceptionlessExceptionHandler.HttpContextSubmittedKey] = new InvalidOperationException("other");
171+
172+
// Act — diagnostic listener sees a different exception instance
173+
var newException = new InvalidOperationException("different");
174+
listener.OnNext(new KeyValuePair<string, object>("Microsoft.AspNetCore.Hosting.UnhandledException", new {
175+
httpContext = context,
176+
exception = newException
177+
}));
178+
179+
// Assert — still submits because it's a different exception
180+
Assert.Single(submittingEvents);
181+
}
182+
140183
private static ExceptionlessClient CreateClient(ICollection<EventSubmittingEventArgs> submittingEvents) {
141184
var client = new ExceptionlessClient(configuration => {
142185
configuration.ApiKey = "test-api-key";

0 commit comments

Comments
 (0)