diff --git a/src/Components/Server/src/Circuits/CircuitRegistry.cs b/src/Components/Server/src/Circuits/CircuitRegistry.cs
index 6b068e56eae1..33d0713415e9 100644
--- a/src/Components/Server/src/Circuits/CircuitRegistry.cs
+++ b/src/Components/Server/src/Circuits/CircuitRegistry.cs
@@ -309,9 +309,20 @@ private Task PauseAndDisposeCircuitEntry(DisconnectedCircuitEntry entry)
private async Task PauseAndDisposeCircuitHost(CircuitHost circuitHost, bool saveStateToClient)
{
- await _circuitPersistenceManager.PauseCircuitAsync(circuitHost, saveStateToClient);
- circuitHost.UnhandledException -= CircuitHost_UnhandledException;
- await circuitHost.DisposeAsync();
+ try
+ {
+ await _circuitPersistenceManager.PauseCircuitAsync(circuitHost, saveStateToClient);
+ }
+ catch (ObjectDisposedException ex)
+ {
+ // Expected when service provider is disposed during circuit cleanup e.g. by forced GC
+ CircuitHost_UnhandledException(circuitHost, new UnhandledExceptionEventArgs(ex, isTerminating: true));
+ }
+ finally
+ {
+ circuitHost.UnhandledException -= CircuitHost_UnhandledException;
+ await circuitHost.DisposeAsync();
+ }
}
internal async Task PauseCircuitAsync(
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Redirections/CircularRedirection.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Redirections/CircularRedirection.razor
index dd4ec0b413e0..e5e2bfe39a65 100644
--- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Redirections/CircularRedirection.razor
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Redirections/CircularRedirection.razor
@@ -13,9 +13,22 @@
{
Unobserved Exceptions (for debugging):
- @foreach (var ex in _unobservedExceptions)
+ @foreach (var exceptionDetail in _unobservedExceptions)
{
- - @ex.ToString()
+ -
+ Observed at: @exceptionDetail.ObservedAt.ToString("yyyy-MM-dd HH:mm:ss.fff") UTC
+ Thread ID: @exceptionDetail.ObservedThreadId
+ From Finalizer Thread: @exceptionDetail.IsFromFinalizerThread
+ Exception: @exceptionDetail.Exception.ToString()
+
+ Detailed Exception Information
+ @exceptionDetail.DetailedExceptionInfo
+
+
+ Call Stack When Observed
+ @exceptionDetail.ObservedCallStack
+
+
}
}
@@ -23,16 +36,16 @@
@code {
private bool _shouldStopRedirecting;
- private IReadOnlyCollection _unobservedExceptions = Array.Empty();
+ private IReadOnlyCollection _unobservedExceptions = Array.Empty();
protected override async Task OnInitializedAsync()
{
- int visits = Observer.GetCircularRedirectCount();
- if (visits == 0)
- {
- // make sure we start with clean logs
- Observer.Clear();
- }
+ int visits = Observer.GetCircularRedirectCount();
+ if (visits == 0)
+ {
+ // make sure we start with clean logs
+ Observer.Clear();
+ }
// Force GC collection to trigger finalizers - this is what causes the issue
GC.Collect();
@@ -47,7 +60,7 @@
else
{
_shouldStopRedirecting = true;
- _unobservedExceptions = Observer.GetExceptions();
+ _unobservedExceptions = Observer.GetExceptionDetails();
}
}
}
diff --git a/src/Components/test/testassets/Components.TestServer/UnobservedTaskExceptionObserver.cs b/src/Components/test/testassets/Components.TestServer/UnobservedTaskExceptionObserver.cs
index af61be6cebd5..0fca588d2b35 100644
--- a/src/Components/test/testassets/Components.TestServer/UnobservedTaskExceptionObserver.cs
+++ b/src/Components/test/testassets/Components.TestServer/UnobservedTaskExceptionObserver.cs
@@ -2,24 +2,134 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Globalization;
+using System.Text;
using System.Threading;
+using System.Linq;
namespace TestServer;
+///
+/// Represents detailed information about an unobserved task exception, including the original call stack.
+///
+public class UnobservedExceptionDetails
+{
+ ///
+ /// The original exception that was unobserved.
+ ///
+ public Exception Exception { get; init; }
+
+ ///
+ /// The timestamp when the exception was observed.
+ ///
+ public DateTime ObservedAt { get; init; }
+
+ ///
+ /// The current call stack when the exception was observed (may show finalizer thread).
+ ///
+ public string ObservedCallStack { get; init; }
+
+ ///
+ /// Detailed breakdown of inner exceptions and their stack traces.
+ ///
+ public string DetailedExceptionInfo { get; init; }
+
+ ///
+ /// The managed thread ID where the exception was observed.
+ ///
+ public int ObservedThreadId { get; init; }
+
+ ///
+ /// Whether this exception was observed on the finalizer thread.
+ ///
+ public bool IsFromFinalizerThread { get; init; }
+
+ public UnobservedExceptionDetails(Exception exception)
+ {
+ Exception = exception;
+ ObservedAt = DateTime.UtcNow;
+ ObservedCallStack = Environment.StackTrace;
+ DetailedExceptionInfo = BuildDetailedExceptionInfo(exception);
+ ObservedThreadId = Environment.CurrentManagedThreadId;
+ IsFromFinalizerThread = Thread.CurrentThread.IsThreadPoolThread && Thread.CurrentThread.IsBackground;
+ }
+
+ private static string BuildDetailedExceptionInfo(Exception exception)
+ {
+ var sb = new StringBuilder();
+ var currentException = exception;
+ var depth = 0;
+
+ while (currentException is not null)
+ {
+ var indent = new string(' ', depth * 2);
+ sb.AppendLine(CultureInfo.InvariantCulture, $"{indent}Exception Type: {currentException.GetType().FullName}");
+ sb.AppendLine(CultureInfo.InvariantCulture, $"{indent}Message: {currentException.Message}");
+
+ if (currentException.Data.Count > 0)
+ {
+ sb.AppendLine(CultureInfo.InvariantCulture, $"{indent}Data:");
+ foreach (var key in currentException.Data.Keys)
+ {
+ sb.AppendLine(CultureInfo.InvariantCulture, $"{indent} {key}: {currentException.Data[key]}");
+ }
+ }
+
+ if (!string.IsNullOrEmpty(currentException.StackTrace))
+ {
+ sb.AppendLine(CultureInfo.InvariantCulture, $"{indent}Stack Trace:");
+ sb.AppendLine(currentException.StackTrace);
+ }
+
+ // Handle AggregateException specially to extract all inner exceptions
+ if (currentException is AggregateException aggregateException)
+ {
+ sb.AppendLine(CultureInfo.InvariantCulture, $"{indent}Aggregate Exception contains {aggregateException.InnerExceptions.Count} inner exceptions:");
+ for (int i = 0; i < aggregateException.InnerExceptions.Count; i++)
+ {
+ sb.AppendLine(CultureInfo.InvariantCulture, $"{indent} Inner Exception {i + 1}:");
+ sb.AppendLine(BuildDetailedExceptionInfo(aggregateException.InnerExceptions[i]));
+ }
+ break; // Don't process InnerException for AggregateException as we've handled all inner exceptions
+ }
+
+ currentException = currentException.InnerException;
+ depth++;
+
+ if (currentException is not null)
+ {
+ sb.AppendLine(CultureInfo.InvariantCulture, $"{indent}--- Inner Exception ---");
+ }
+ }
+
+ return sb.ToString();
+ }
+}
+
public class UnobservedTaskExceptionObserver
{
- private readonly ConcurrentQueue _exceptions = new();
+ private readonly ConcurrentQueue _exceptions = new();
private int _circularRedirectCount;
public void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
- _exceptions.Enqueue(e.Exception);
+ var details = new UnobservedExceptionDetails(e.Exception);
+ _exceptions.Enqueue(details);
e.SetObserved(); // Mark as observed to prevent the process from crashing during tests
}
public bool HasExceptions => !_exceptions.IsEmpty;
- public IReadOnlyCollection GetExceptions() => _exceptions.ToArray();
+ ///
+ /// Gets the detailed exception information including original call stacks.
+ ///
+ public IReadOnlyCollection GetExceptionDetails() => _exceptions.ToArray();
+
+ ///
+ /// Gets the raw exceptions for backward compatibility.
+ ///
+ public IReadOnlyCollection GetExceptions() => _exceptions.ToArray().Select(d => d.Exception).ToList();
public void Clear()
{
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy