From 9857eef67426ba2ec45d4393b6d676d101faed3a Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 6 Sep 2023 14:41:12 +0200 Subject: [PATCH 01/22] Add the ability to create a separate DI scope --- .../ExceptionHandlerExtensions.cs | 19 +++++++++++++++++++ .../ExceptionHandlerMiddlewareImpl.cs | 14 ++++++++++++++ .../ExceptionHandlerOptions.cs | 7 +++++++ .../Diagnostics/src/PublicAPI.Unshipped.txt | 3 +++ 4 files changed, 43 insertions(+) diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs index 6256709900ba..88199a9f358b 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs @@ -47,6 +47,25 @@ public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder a }); } + /// + /// Adds a middleware to the pipeline that will catch exceptions, log them, reset the request path, and re-execute the request. + /// The request will not be re-executed if the response has already started. + /// + /// The . + /// The path to the endpoint that will handle the exception. + /// Whether or not to create a new scope. + /// + public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, string errorHandlingPath, bool newScope) + { + ArgumentNullException.ThrowIfNull(app); + + return app.UseExceptionHandler(new ExceptionHandlerOptions + { + ExceptionHandlingPath = new PathString(errorHandlingPath), + UseNewServiceResolutionScope = newScope + }); + } + /// /// Adds a middleware to the pipeline that will catch exceptions, log them, and re-execute the request in an alternate pipeline. /// The request will not be re-executed if the response has already started. diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs index c2063112d2d7..69ba4cdd3ade 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Diagnostics; @@ -70,6 +71,7 @@ public ExceptionHandlerMiddlewareImpl( public Task Invoke(HttpContext context) { ExceptionDispatchInfo edi; + try { var task = _next(context); @@ -141,8 +143,16 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed { context.Request.Path = _options.ExceptionHandlingPath; } + var oldScope = _options.UseNewServiceResolutionScope ? context.RequestServices : null; + using var scope = _options.UseNewServiceResolutionScope ? context.RequestServices.GetRequiredService().CreateScope() : null; + try { + if (scope != null) + { + context.RequestServices = scope.ServiceProvider; + } + var exceptionHandlerFeature = new ExceptionHandlerFeature() { Error = edi.SourceException, @@ -216,6 +226,10 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed finally { context.Request.Path = originalPath; + if (oldScope != null) + { + context.RequestServices = oldScope; + } } _metrics.RequestException(exceptionName, ExceptionResult.Unhandled, handler: null); diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs index d776813f29d1..1ca4582e5f84 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs @@ -17,6 +17,13 @@ public class ExceptionHandlerOptions /// public PathString ExceptionHandlingPath { get; set; } + /// + /// Gets or sets whether the handler needs to create a separate scope and + /// replace it on . + /// + /// The default value is . + public bool UseNewServiceResolutionScope { get; set; } + /// /// The that will handle the exception. If this is not /// explicitly provided, the subsequent middleware pipeline will be used by default. diff --git a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt index 7ace23668c8d..8db559d5a430 100644 --- a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt @@ -1,5 +1,8 @@ #nullable enable +Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.UseNewServiceResolutionScope.get -> bool +Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.UseNewServiceResolutionScope.set -> void Microsoft.AspNetCore.Diagnostics.IExceptionHandler Microsoft.AspNetCore.Diagnostics.IExceptionHandler.TryHandleAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, System.Exception! exception, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.OriginalStatusCode.get -> int +static Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, string! errorHandlingPath, bool newScope) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! static Microsoft.Extensions.DependencyInjection.ExceptionHandlerServiceCollectionExtensions.AddExceptionHandler(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! From b6f529623354df5f6fa8e2f3537a8a4a0028a82d Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 6 Sep 2023 15:43:07 +0200 Subject: [PATCH 02/22] Supress interactive rendering on error scenarios --- ...oft.AspNetCore.Components.Endpoints.csproj | 1 + .../EndpointHtmlRenderer.Streaming.cs | 3 +- .../src/Rendering/EndpointHtmlRenderer.cs | 2 ++ .../Samples/BlazorUnitedApp/Pages/Error.razor | 35 +++++++++++++++++++ .../Samples/BlazorUnitedApp/Pages/Index.razor | 2 +- .../Samples/BlazorUnitedApp/Program.cs | 7 +++- 6 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 src/Components/Samples/BlazorUnitedApp/Pages/Error.razor diff --git a/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj b/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj index 05eeca0222c1..8d1d9fb09568 100644 --- a/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj +++ b/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj @@ -49,6 +49,7 @@ + diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index 31ef21b0ad57..8e9098acf147 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -19,6 +19,7 @@ internal partial class EndpointHtmlRenderer private TextWriter? _streamingUpdatesWriter; private HashSet? _visitedComponentIdsInCurrentStreamingBatch; private string? _ssrFramingCommentMarkup; + private bool _allowBoundaryMarkers; public void InitializeStreamingRenderingFraming(HttpContext httpContext) { @@ -202,7 +203,7 @@ protected override void RenderChildComponent(TextWriter output, ref RenderTreeFr { var componentId = componentFrame.ComponentId; var sequenceAndKey = new SequenceAndKey(componentFrame.Sequence, componentFrame.ComponentKey); - WriteComponentHtml(componentId, output, allowBoundaryMarkers: true, sequenceAndKey); + WriteComponentHtml(componentId, output, _allowBoundaryMarkers, sequenceAndKey); } private void WriteComponentHtml(int componentId, TextWriter output, bool allowBoundaryMarkers, SequenceAndKey sequenceAndKey = default) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 8031ca9e268a..4be85f0d82bd 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Routing; @@ -59,6 +60,7 @@ private void SetHttpContext(HttpContext httpContext) if (_httpContext is null) { _httpContext = httpContext; + _allowBoundaryMarkers = _httpContext.Features.Get() is null; } else if (_httpContext != httpContext) { diff --git a/src/Components/Samples/BlazorUnitedApp/Pages/Error.razor b/src/Components/Samples/BlazorUnitedApp/Pages/Error.razor new file mode 100644 index 000000000000..ee6f5865a0c1 --- /dev/null +++ b/src/Components/Samples/BlazorUnitedApp/Pages/Error.razor @@ -0,0 +1,35 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] public HttpContext? HttpContext { get; set; } + + public string? RequestId { get; set; } + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor b/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor index 6604c511e2ba..293d1d407448 100644 --- a/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor +++ b/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor @@ -33,7 +33,7 @@ [SupplyParameterFromForm] Customer? Value { get; set; } - protected override void OnInitialized() => Value ??= new(); + protected override void OnInitialized() => throw new InvalidOperationException("Some error"); bool _submitted = false; public void Submit() => _submitted = true; diff --git a/src/Components/Samples/BlazorUnitedApp/Program.cs b/src/Components/Samples/BlazorUnitedApp/Program.cs index 8a772d66e1cb..a64431edb7d0 100644 --- a/src/Components/Samples/BlazorUnitedApp/Program.cs +++ b/src/Components/Samples/BlazorUnitedApp/Program.cs @@ -3,8 +3,13 @@ using BlazorUnitedApp; using BlazorUnitedApp.Data; +using Microsoft.AspNetCore.Hosting.StaticWebAssets; var builder = WebApplication.CreateBuilder(args); +// Enable this so that we can resolve static web assets in the Production environment +// without publishing the app. +// This is so that we can test the error page. +StaticWebAssetsLoader.UseStaticWebAssets(builder.Environment, builder.Configuration); // Add services to the container. builder.Services.AddRazorComponents(); @@ -16,7 +21,7 @@ // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { - app.UseExceptionHandler("/Error"); + app.UseExceptionHandler("/Error", newScope: true); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } From f1357aa9d6dd2259cc23d631fb6ff0155337808a Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 7 Sep 2023 15:55:16 +0200 Subject: [PATCH 03/22] Revert sample changes --- src/Components/Samples/BlazorUnitedApp/Pages/Index.razor | 2 +- src/Components/Samples/BlazorUnitedApp/Program.cs | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor b/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor index 293d1d407448..6604c511e2ba 100644 --- a/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor +++ b/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor @@ -33,7 +33,7 @@ [SupplyParameterFromForm] Customer? Value { get; set; } - protected override void OnInitialized() => throw new InvalidOperationException("Some error"); + protected override void OnInitialized() => Value ??= new(); bool _submitted = false; public void Submit() => _submitted = true; diff --git a/src/Components/Samples/BlazorUnitedApp/Program.cs b/src/Components/Samples/BlazorUnitedApp/Program.cs index a64431edb7d0..8a772d66e1cb 100644 --- a/src/Components/Samples/BlazorUnitedApp/Program.cs +++ b/src/Components/Samples/BlazorUnitedApp/Program.cs @@ -3,13 +3,8 @@ using BlazorUnitedApp; using BlazorUnitedApp.Data; -using Microsoft.AspNetCore.Hosting.StaticWebAssets; var builder = WebApplication.CreateBuilder(args); -// Enable this so that we can resolve static web assets in the Production environment -// without publishing the app. -// This is so that we can test the error page. -StaticWebAssetsLoader.UseStaticWebAssets(builder.Environment, builder.Configuration); // Add services to the container. builder.Services.AddRazorComponents(); @@ -21,7 +16,7 @@ // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { - app.UseExceptionHandler("/Error", newScope: true); + app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } From a632314e09ef7eb860fdb51c6c6d02f4070c5878 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 7 Sep 2023 15:59:59 +0200 Subject: [PATCH 04/22] Feedback --- .../src/ExceptionHandler/ExceptionHandlerExtensions.cs | 2 +- .../src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs | 4 ++-- .../src/ExceptionHandler/ExceptionHandlerOptions.cs | 4 ++-- src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs index 88199a9f358b..ff92d70a56c7 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs @@ -62,7 +62,7 @@ public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder a return app.UseExceptionHandler(new ExceptionHandlerOptions { ExceptionHandlingPath = new PathString(errorHandlingPath), - UseNewServiceResolutionScope = newScope + CreateScopeForErrors = newScope }); } diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs index 69ba4cdd3ade..e8f40e9b3fab 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs @@ -143,8 +143,8 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed { context.Request.Path = _options.ExceptionHandlingPath; } - var oldScope = _options.UseNewServiceResolutionScope ? context.RequestServices : null; - using var scope = _options.UseNewServiceResolutionScope ? context.RequestServices.GetRequiredService().CreateScope() : null; + var oldScope = _options.CreateScopeForErrors ? context.RequestServices : null; + using var scope = _options.CreateScopeForErrors ? context.RequestServices.GetRequiredService().CreateScope() : null; try { diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs index 1ca4582e5f84..1b322019211d 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs @@ -19,10 +19,10 @@ public class ExceptionHandlerOptions /// /// Gets or sets whether the handler needs to create a separate scope and - /// replace it on . + /// replace it on when re-executing the request to handle an error. /// /// The default value is . - public bool UseNewServiceResolutionScope { get; set; } + public bool CreateScopeForErrors { get; set; } /// /// The that will handle the exception. If this is not diff --git a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt index 8db559d5a430..cc24bda3d1b0 100644 --- a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt @@ -1,6 +1,6 @@ #nullable enable -Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.UseNewServiceResolutionScope.get -> bool -Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.UseNewServiceResolutionScope.set -> void +Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.CreateScopeForErrors.get -> bool +Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.CreateScopeForErrors.set -> void Microsoft.AspNetCore.Diagnostics.IExceptionHandler Microsoft.AspNetCore.Diagnostics.IExceptionHandler.TryHandleAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, System.Exception! exception, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.OriginalStatusCode.get -> int From a7d5c30c1da132a4ee6c70b89a863ffe1ba411c4 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 12 Sep 2023 17:31:45 +0200 Subject: [PATCH 05/22] Add error page --- .../Components/Pages/Error.razor | 35 +++++++++++++++++++ .../BlazorWeb-CSharp/Program.cs | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Error.razor diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Error.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Error.razor new file mode 100644 index 000000000000..e47ddab18085 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Error.razor @@ -0,0 +1,35 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] public HttpContext? HttpContext { get; set; } + + public string? RequestId { get; set; } + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Program.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Program.cs index 5470d2ce484d..effb8552ca35 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Program.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Program.cs @@ -33,7 +33,7 @@ if (!app.Environment.IsDevelopment()) #endif { - app.UseExceptionHandler("/Error"); + app.UseExceptionHandler("/Error", createScopeForErrors: true); #if (HasHttpsProfile) // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); From 2634e67733718e2ae402489cdafa018acdf368fc Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 12 Sep 2023 18:10:15 +0200 Subject: [PATCH 06/22] Add E2E test --- .../ServerRenderingTests/ErrorHandlingTest.cs | 40 +++++++++++++++++++ .../Properties/launchSettings.json | 2 +- .../RazorComponentEndpointsStartup.cs | 5 +++ .../RazorComponents/Pages/Error.razor | 35 ++++++++++++++++ .../Pages/PageThatThrows.razor | 11 +++++ .../ExceptionHandlerExtensions.cs | 6 +-- 6 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 src/Components/test/E2ETest/ServerRenderingTests/ErrorHandlingTest.cs create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Error.razor create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatThrows.razor diff --git a/src/Components/test/E2ETest/ServerRenderingTests/ErrorHandlingTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/ErrorHandlingTest.cs new file mode 100644 index 000000000000..eac5ef36b398 --- /dev/null +++ b/src/Components/test/E2ETest/ServerRenderingTests/ErrorHandlingTest.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using TestServer; +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.E2ETesting; +using Xunit.Abstractions; +using OpenQA.Selenium; + +namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests; + +public class ErrorHandlingTest(BrowserFixture browserFixture, BasicTestAppServerSiteFixture> serverFixture, ITestOutputHelper output) + : ServerTestBase>>(browserFixture, serverFixture, output) +{ + + [Fact] + public void RendersExceptionFromComponent() + { + GoTo("Throws"); + + Browser.Equal("Error", () => Browser.Title); + + Assert.Collection( + Browser.FindElements(By.CssSelector(".text-danger")), + item => Assert.Equal("Error.", item.Text), + item => Assert.Equal("An error occurred while processing your request.", item.Text)); + } + + private void GoTo(string relativePath) + { + Navigate($"{ServerPathBase}/{relativePath}"); + } +} diff --git a/src/Components/test/testassets/Components.TestServer/Properties/launchSettings.json b/src/Components/test/testassets/Components.TestServer/Properties/launchSettings.json index 38847dc2186f..efa3251c5cd5 100644 --- a/src/Components/test/testassets/Components.TestServer/Properties/launchSettings.json +++ b/src/Components/test/testassets/Components.TestServer/Properties/launchSettings.json @@ -4,7 +4,7 @@ "commandName": "Project", "launchBrowser": true, "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_ENVIRONMENT": "Production", // When started manually (not by E2E tests), it's helpful to run each of the child apps // on deterministic port numbers so that you don't have to keep rediscovering it every diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index f2f69e00df34..0a35b579d26e 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -51,6 +51,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.Map("/subdir", app => { + if (!env.IsDevelopment()) + { + app.UseExceptionHandler("/Error", createScopeForErrors: true); + } + app.UseStaticFiles(); app.UseRouting(); UseFakeAuthState(app); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Error.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Error.razor new file mode 100644 index 000000000000..9f2edf8226dc --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Error.razor @@ -0,0 +1,35 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code { + [CascadingParameter] public HttpContext? HttpContext { get; set; } + + public string? RequestId { get; set; } + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatThrows.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatThrows.razor new file mode 100644 index 000000000000..853c8baedef8 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatThrows.razor @@ -0,0 +1,11 @@ +@page "/Throws" + +Page that throws + +

This page throws during OnInitialize to showcase error handling via UseExceptionHandler.

+ +@code +{ + protected override void OnInitialized() => + throw new InvalidOperationException("This page throws on purpose."); +} diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs index ff92d70a56c7..fafeec524e72 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs @@ -53,16 +53,16 @@ public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder a ///
/// The . /// The path to the endpoint that will handle the exception. - /// Whether or not to create a new scope. + /// Whether or not to create a new scope. /// - public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, string errorHandlingPath, bool newScope) + public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, string errorHandlingPath, bool createScopeForErrors) { ArgumentNullException.ThrowIfNull(app); return app.UseExceptionHandler(new ExceptionHandlerOptions { ExceptionHandlingPath = new PathString(errorHandlingPath), - CreateScopeForErrors = newScope + CreateScopeForErrors = createScopeForErrors }); } From 50cc0e060416cfa614338a4f226c1c7db0f7d6d2 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 12 Sep 2023 18:15:28 +0200 Subject: [PATCH 07/22] Fix api baseline --- src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt index cc24bda3d1b0..f82528f7d333 100644 --- a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt @@ -4,5 +4,5 @@ Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.CreateScopeForErrors.set -> Microsoft.AspNetCore.Diagnostics.IExceptionHandler Microsoft.AspNetCore.Diagnostics.IExceptionHandler.TryHandleAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, System.Exception! exception, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.OriginalStatusCode.get -> int -static Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, string! errorHandlingPath, bool newScope) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, string! errorHandlingPath, bool createScopeForErrors) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! static Microsoft.Extensions.DependencyInjection.ExceptionHandlerServiceCollectionExtensions.AddExceptionHandler(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! From e42d93dbeacefa8517355e004ad3b1d9fe7b1c9f Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 7 Sep 2023 17:05:51 +0200 Subject: [PATCH 08/22] Turn scope by default for testing purposes --- .../ExceptionHandlerOptions.cs | 2 +- .../ExceptionHandlerMiddlewareTest.cs | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs index 1b322019211d..6b605c4c79e3 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs @@ -22,7 +22,7 @@ public class ExceptionHandlerOptions /// replace it on when re-executing the request to handle an error. ///
/// The default value is . - public bool CreateScopeForErrors { get; set; } + public bool CreateScopeForErrors { get; set; } = true; /// /// The that will handle the exception. If this is not diff --git a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs index 88c55e7a517d..33bcfdb91a55 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs @@ -396,7 +396,29 @@ private class TestServiceProvider : IServiceProvider { public object GetService(Type serviceType) { + if (serviceType == typeof(IServiceScopeFactory)) + { + return new TestServiceScopeFactory(); + } + throw new NotImplementedException(); } } + + private class TestServiceScopeFactory : IServiceScopeFactory + { + public IServiceScope CreateScope() + { + return new TestServiceScope(); + } + } + + private class TestServiceScope : IServiceScope + { + public IServiceProvider ServiceProvider => new TestServiceProvider(); + + public void Dispose() + { + } + } } From 3b58a6581579f46da3d49f33788f6f1e90740479 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 7 Sep 2023 20:54:27 +0200 Subject: [PATCH 09/22] Revert "Turn scope by default for testing purposes" This reverts commit bd6134c6e6768084be7073abe120cad3289160ad. --- .../ExceptionHandlerOptions.cs | 2 +- .../ExceptionHandlerMiddlewareTest.cs | 22 ------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs index 6b605c4c79e3..1b322019211d 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs @@ -22,7 +22,7 @@ public class ExceptionHandlerOptions /// replace it on when re-executing the request to handle an error. /// /// The default value is . - public bool CreateScopeForErrors { get; set; } = true; + public bool CreateScopeForErrors { get; set; } /// /// The that will handle the exception. If this is not diff --git a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs index 33bcfdb91a55..88c55e7a517d 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs @@ -396,29 +396,7 @@ private class TestServiceProvider : IServiceProvider { public object GetService(Type serviceType) { - if (serviceType == typeof(IServiceScopeFactory)) - { - return new TestServiceScopeFactory(); - } - throw new NotImplementedException(); } } - - private class TestServiceScopeFactory : IServiceScopeFactory - { - public IServiceScope CreateScope() - { - return new TestServiceScope(); - } - } - - private class TestServiceScope : IServiceScope - { - public IServiceProvider ServiceProvider => new TestServiceProvider(); - - public void Dispose() - { - } - } } From 4faecfcb94b3fcedb1815bb631b5e096a9436003 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 13 Sep 2023 15:26:38 +0200 Subject: [PATCH 10/22] Prevent form handling logic to run during exception handling --- .../Endpoints/src/RazorComponentEndpointInvoker.cs | 8 +++++++- .../FormWithParentBindingContextTest.cs | 12 +++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 48f9eacfe104..22510ff9dc0a 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -7,6 +7,7 @@ using System.Text.Encodings.Web; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Components.Endpoints.Rendering; +using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; @@ -133,7 +134,12 @@ await EndpointHtmlRenderer.InitializeStandardComponentServicesAsync( private async Task ValidateRequestAsync(HttpContext context, IAntiforgery? antiforgery) { - var isPost = HttpMethods.IsPost(context.Request.Method); + var isPost = HttpMethods.IsPost(context.Request.Method) && + // Disable POST functionality during exception handling. + // The exception handler middleware will not update the request method, and we don't + // want to run the form handling logic against the error page. + context.Features.Get() == null; + if (isPost) { var valid = false; diff --git a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs index 82110a7b7d64..968732849362 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs @@ -1151,15 +1151,13 @@ private void AssertHasInternalServerError(bool suppressedEnhancedNavigation, boo { Browser.True(() => Browser.FindElement(By.TagName("html")).Text.Contains("There was an unhandled exception on the current request")); } - else if (suppressedEnhancedNavigation) - { - // Chrome's built-in error UI for a 500 response when there's no response content - Browser.Exists(By.Id("main-frame-error")); - } else { - // The UI generated by enhanced nav when there's no response content - Browser.Contains("Error: 500", () => Browser.Exists(By.TagName("html")).Text); + // Displays the error page from the exception handler + Assert.Collection( + Browser.FindElements(By.CssSelector(".text-danger")), + item => Assert.Equal("Error.", item.Text), + item => Assert.Equal("An error occurred while processing your request.", item.Text)); } } From 4468cd5ec076f1e11ef1b1ec33c11d07c3b21280 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 13 Sep 2023 19:48:55 +0200 Subject: [PATCH 11/22] Update template baselines --- .../test/Templates.Tests/template-baselines.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json index 6b982d80f7b2..7a385dfe2d88 100644 --- a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json +++ b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json @@ -519,6 +519,7 @@ "Components/Layout/MainLayout.razor.css", "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", + "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Components/Pages/Weather.razor", "Properties/launchSettings.json", @@ -545,6 +546,7 @@ "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", "Components/Pages/Counter.razor", + "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Components/Pages/Weather.razor", "Properties/launchSettings.json", @@ -571,6 +573,7 @@ "{ProjectName}/Components/Layout/MainLayout.razor.css", "{ProjectName}/Components/Layout/NavMenu.razor", "{ProjectName}/Components/Layout/NavMenu.razor.css", + "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", "{ProjectName}/Components/Pages/Weather.razor", "{ProjectName}/Properties/launchSettings.json", @@ -603,6 +606,7 @@ "{ProjectName}/Components/Layout/MainLayout.razor.css", "{ProjectName}/Components/Layout/NavMenu.razor", "{ProjectName}/Components/Layout/NavMenu.razor.css", + "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", "{ProjectName}/Components/Pages/Weather.razor", "{ProjectName}/Properties/launchSettings.json", @@ -722,6 +726,7 @@ "Components/_Imports.razor", "Components/Layout/MainLayout.razor", "Components/Layout/MainLayout.razor.css", + "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Properties/launchSettings.json", "wwwroot/app.css" @@ -741,6 +746,7 @@ "Components/_Imports.razor", "Components/Layout/MainLayout.razor", "Components/Layout/MainLayout.razor.css", + "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Properties/launchSettings.json", "wwwroot/app.css" @@ -757,6 +763,7 @@ "{ProjectName}/{ProjectName}.csproj", "{ProjectName}/Program.cs", "{ProjectName}/Components/App.razor", + "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Routes.razor", "{ProjectName}/Components/_Imports.razor", "{ProjectName}/Components/Layout/MainLayout.razor", @@ -784,6 +791,7 @@ "{ProjectName}/Components/_Imports.razor", "{ProjectName}/Components/Layout/MainLayout.razor", "{ProjectName}/Components/Layout/MainLayout.razor.css", + "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", "{ProjectName}/Properties/launchSettings.json", "{ProjectName}/wwwroot/app.css", From 571c1cf4e1ed98647284e2b288d0de0bd41da905 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 19 Sep 2023 21:16:37 +0200 Subject: [PATCH 12/22] Fixes --- .../src/RazorComponentEndpointInvoker.cs | 17 ++++++++++++----- .../Rendering/EndpointHtmlRenderer.Streaming.cs | 13 +++++++------ .../src/Rendering/EndpointHtmlRenderer.cs | 2 -- .../src/Results/RazorComponentResultExecutor.cs | 4 +++- .../ServerRenderingTests/ErrorHandlingTest.cs | 1 + .../Components.TestServer.csproj | 6 ++++++ .../RazorComponents/Pages/Error.razor | 1 + .../RazorComponents/Pages/ErrorLayout.razor | 7 +++++++ 8 files changed, 37 insertions(+), 14 deletions(-) create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ErrorLayout.razor diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 22510ff9dc0a..8cbbed1d90aa 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -35,8 +35,12 @@ public Task Render(HttpContext context) private async Task RenderComponentCore(HttpContext context) { context.Response.ContentType = RazorComponentResultExecutor.DefaultContentType; - _renderer.InitializeStreamingRenderingFraming(context); - EndpointHtmlRenderer.MarkAsAllowingEnhancedNavigation(context); + var isErrorHandler = context.Features.Get() is not null; + _renderer.InitializeStreamingRenderingFraming(context, isErrorHandler); + if (!isErrorHandler) + { + EndpointHtmlRenderer.MarkAsAllowingEnhancedNavigation(context); + } var endpoint = context.GetEndpoint() ?? throw new InvalidOperationException($"An endpoint must be set on the '{nameof(HttpContext)}'."); @@ -84,7 +88,7 @@ await EndpointHtmlRenderer.InitializeStandardComponentServicesAsync( context, rootComponent, ParameterView.Empty, - waitForQuiescence: result.IsPost); + waitForQuiescence: result.IsPost || isErrorHandler); Task quiesceTask; if (!result.IsPost) @@ -123,8 +127,11 @@ await EndpointHtmlRenderer.InitializeStandardComponentServicesAsync( } // Emit comment containing state. - var componentStateHtmlContent = await _renderer.PrerenderPersistedStateAsync(context); - componentStateHtmlContent.WriteTo(bufferWriter, HtmlEncoder.Default); + if (!isErrorHandler) + { + var componentStateHtmlContent = await _renderer.PrerenderPersistedStateAsync(context); + componentStateHtmlContent.WriteTo(bufferWriter, HtmlEncoder.Default); + } // Invoke FlushAsync to ensure any buffered content is asynchronously written to the underlying // response asynchronously. In the absence of this line, the buffer gets synchronously written to the diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index 8e9098acf147..d24a1357cbe6 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -19,10 +19,11 @@ internal partial class EndpointHtmlRenderer private TextWriter? _streamingUpdatesWriter; private HashSet? _visitedComponentIdsInCurrentStreamingBatch; private string? _ssrFramingCommentMarkup; - private bool _allowBoundaryMarkers; + private bool _isHandlingErrors; - public void InitializeStreamingRenderingFraming(HttpContext httpContext) + public void InitializeStreamingRenderingFraming(HttpContext httpContext, bool isErrorHandler) { + _isHandlingErrors = isErrorHandler; if (IsProgressivelyEnhancedNavigation(httpContext.Request)) { var id = Guid.NewGuid().ToString(); @@ -203,7 +204,7 @@ protected override void RenderChildComponent(TextWriter output, ref RenderTreeFr { var componentId = componentFrame.ComponentId; var sequenceAndKey = new SequenceAndKey(componentFrame.Sequence, componentFrame.ComponentKey); - WriteComponentHtml(componentId, output, _allowBoundaryMarkers, sequenceAndKey); + WriteComponentHtml(componentId, output, allowBoundaryMarkers: true, sequenceAndKey); } private void WriteComponentHtml(int componentId, TextWriter output, bool allowBoundaryMarkers, SequenceAndKey sequenceAndKey = default) @@ -211,11 +212,11 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo _visitedComponentIdsInCurrentStreamingBatch?.Add(componentId); var componentState = (EndpointComponentState)GetComponentState(componentId); - var renderBoundaryMarkers = allowBoundaryMarkers && componentState.StreamRendering; + var renderBoundaryMarkers = allowBoundaryMarkers && componentState.StreamRendering && !_isHandlingErrors; ComponentEndMarker? endMarkerOrNull = default; - if (componentState.Component is SSRRenderModeBoundary boundary) + if (componentState.Component is SSRRenderModeBoundary boundary && !_isHandlingErrors) { var marker = boundary.ToMarker(_httpContext, sequenceAndKey.Sequence, sequenceAndKey.Key); endMarkerOrNull = marker.ToEndMarker(); @@ -247,7 +248,7 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo output.Write("-->"); } - if (endMarkerOrNull is { } endMarker) + if (endMarkerOrNull is { } endMarker && !_isHandlingErrors) { var serializedEndRecord = JsonSerializer.Serialize(endMarker, ServerComponentSerializationSettings.JsonSerializationOptions); output.Write(""); } - if (endMarkerOrNull is { } endMarker && !_isHandlingErrors) + if (endMarkerOrNull is { } endMarker) { var serializedEndRecord = JsonSerializer.Serialize(endMarker, ServerComponentSerializationSettings.JsonSerializationOptions); output.Write("