diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index 16c44c92f641..abc9441d54fa 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -229,9 +229,16 @@ async Task Execute() // Clear all pending work. _nonStreamingPendingTasks.Clear(); - // new work might be added before we check again as a result of waiting for all - // the child components to finish executing SetParametersAsync - await pendingWork; + try + { + // new work might be added before we check again as a result of waiting for all + // the child components to finish executing SetParametersAsync + await pendingWork; + } + catch (NavigationException navigationException) + { + await HandleNavigationException(_httpContext, navigationException); + } } } } diff --git a/src/Components/test/E2ETest/ServerRenderingTests/RedirectionTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/RedirectionTest.cs index f3b70bc87509..e7830517b3ef 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/RedirectionTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/RedirectionTest.cs @@ -221,6 +221,18 @@ public void RedirectEnhancedGetToInternalWithErrorBoundary() Assert.EndsWith("/subdir/redirect", Browser.Url); } + [Fact] + public void NavigationException_InAsyncContext_DoesNotBecomeUnobservedTaskException() + { + AppContext.SetSwitch("Microsoft.AspNetCore.Components.Endpoints.NavigationManager.DisableThrowNavigationException", false); + + // Navigate to the page that triggers the circular redirect. + Navigate($"{ServerPathBase}/redirect/circular"); + + // The component will stop redirecting after 3 attempts and render the exception count. + Browser.Equal("0", () => Browser.FindElement(By.Id("unobserved-exceptions-count")).Text); + } + private void AssertElementRemoved(IWebElement element) { Browser.True(() => diff --git a/src/Components/test/testassets/Components.TestServer/Program.cs b/src/Components/test/testassets/Components.TestServer/Program.cs index 800ad27ddce4..568f809454a7 100644 --- a/src/Components/test/testassets/Components.TestServer/Program.cs +++ b/src/Components/test/testassets/Components.TestServer/Program.cs @@ -81,8 +81,16 @@ private static string[] CreateAdditionalArgs(string[] args) => public static IHost BuildWebHost(string[] args) => BuildWebHost(args); - public static IHost BuildWebHost(string[] args) where TStartup : class => - Host.CreateDefaultBuilder(args) + public static IHost BuildWebHost(string[] args) where TStartup : class + { + var unobservedTaskExceptionObserver = new UnobservedTaskExceptionObserver(); + TaskScheduler.UnobservedTaskException += unobservedTaskExceptionObserver.OnUnobservedTaskException; + + return Host.CreateDefaultBuilder(args) + .ConfigureServices(services => + { + services.AddSingleton(unobservedTaskExceptionObserver); + }) .ConfigureLogging((ctx, lb) => { TestSink sink = new TestSink(); @@ -98,6 +106,7 @@ public static IHost BuildWebHost(string[] args) where TStartup : class webHostBuilder.UseStaticWebAssets(); }) .Build(); + } private static int GetNextChildAppPortNumber() { 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 new file mode 100644 index 000000000000..dd4ec0b413e0 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Redirections/CircularRedirection.razor @@ -0,0 +1,53 @@ +@page "/redirect/circular" +@using System.Collections.Concurrent +@inject NavigationManager Nav +@inject UnobservedTaskExceptionObserver Observer + +

Hello, world!

+ +@if (_shouldStopRedirecting) +{ +

@_unobservedExceptions.Count

+ + @if (_unobservedExceptions.Any()) + { +

Unobserved Exceptions (for debugging):

+
    + @foreach (var ex in _unobservedExceptions) + { +
  • @ex.ToString()
  • + } +
+ } +} + +@code { + private bool _shouldStopRedirecting; + 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(); + } + + // Force GC collection to trigger finalizers - this is what causes the issue + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + await Task.Yield(); + + if (Observer.GetAndIncrementCircularRedirectCount() < 3) + { + Nav.NavigateTo("redirect/circular"); + } + else + { + _shouldStopRedirecting = true; + _unobservedExceptions = Observer.GetExceptions(); + } + } +} diff --git a/src/Components/test/testassets/Components.TestServer/UnobservedTaskExceptionObserver.cs b/src/Components/test/testassets/Components.TestServer/UnobservedTaskExceptionObserver.cs new file mode 100644 index 000000000000..af61be6cebd5 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/UnobservedTaskExceptionObserver.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Threading; + +namespace TestServer; + +public class UnobservedTaskExceptionObserver +{ + private readonly ConcurrentQueue _exceptions = new(); + private int _circularRedirectCount; + + public void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) + { + _exceptions.Enqueue(e.Exception); + e.SetObserved(); // Mark as observed to prevent the process from crashing during tests + } + + public bool HasExceptions => !_exceptions.IsEmpty; + + public IReadOnlyCollection GetExceptions() => _exceptions.ToArray(); + + public void Clear() + { + _exceptions.Clear(); + _circularRedirectCount = 0; + } + + public int GetCircularRedirectCount() + { + return _circularRedirectCount; + } + + public int GetAndIncrementCircularRedirectCount() + { + return Interlocked.Increment(ref _circularRedirectCount) - 1; + } +}