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/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 48f9eacfe104..fbe742dca173 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; @@ -34,7 +35,12 @@ public Task Render(HttpContext context) private async Task RenderComponentCore(HttpContext context) { context.Response.ContentType = RazorComponentResultExecutor.DefaultContentType; - _renderer.InitializeStreamingRenderingFraming(context); + var isErrorHandler = context.Features.Get() is not null; + if (isErrorHandler) + { + Log.InteractivityDisabledForErrorHandling(_logger); + } + _renderer.InitializeStreamingRenderingFraming(context, isErrorHandler); EndpointHtmlRenderer.MarkAsAllowingEnhancedNavigation(context); var endpoint = context.GetEndpoint() ?? throw new InvalidOperationException($"An endpoint must be set on the '{nameof(HttpContext)}'."); @@ -42,7 +48,7 @@ private async Task RenderComponentCore(HttpContext context) var rootComponent = endpoint.Metadata.GetRequiredMetadata().Type; var pageComponent = endpoint.Metadata.GetRequiredMetadata().Type; - Log.BeginRenderComponent(_logger, rootComponent.Name, pageComponent.Name); + Log.BeginRenderRootComponent(_logger, rootComponent.Name, pageComponent.Name); // Metadata controls whether we require antiforgery protection for this endpoint or we should skip it. // The default for razor component endpoints is to require the metadata, but it can be overriden by @@ -83,7 +89,7 @@ await EndpointHtmlRenderer.InitializeStandardComponentServicesAsync( context, rootComponent, ParameterView.Empty, - waitForQuiescence: result.IsPost); + waitForQuiescence: result.IsPost || isErrorHandler); Task quiesceTask; if (!result.IsPost) @@ -122,8 +128,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 @@ -133,8 +142,13 @@ await EndpointHtmlRenderer.InitializeStandardComponentServicesAsync( private async Task ValidateRequestAsync(HttpContext context, IAntiforgery? antiforgery) { - var isPost = HttpMethods.IsPost(context.Request.Method); - if (isPost) + var processPost = 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 (processPost) { var valid = false; // Respect the token validation done by the middleware _if_ it has been set, otherwise @@ -187,7 +201,7 @@ private async Task ValidateRequestAsync(HttpContext cont await context.Request.ReadFormAsync(); var handler = GetFormHandler(context, out var isBadRequest); - return new(valid && !isBadRequest, isPost, handler); + return new(valid && !isBadRequest, processPost, handler); } return RequestValidationState.ValidNonPostRequest; @@ -231,22 +245,25 @@ private string GetDebuggerDisplay() public static partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Begin render root component '{componentType}' with page '{pageType}'.", EventName = "BeginRenderRootComponent")] - public static partial void BeginRenderComponent(ILogger logger, string componentType, string pageType); + [LoggerMessage(1, LogLevel.Debug, "Begin render root component '{componentType}' with page '{pageType}'.", EventName = nameof(BeginRenderRootComponent))] + public static partial void BeginRenderRootComponent(ILogger logger, string componentType, string pageType); - [LoggerMessage(2, LogLevel.Debug, "The antiforgery middleware already failed to validate the current token.", EventName = "MiddlewareAntiforgeryValidationFailed")] + [LoggerMessage(2, LogLevel.Debug, "The antiforgery middleware already failed to validate the current token.", EventName = nameof(MiddlewareAntiforgeryValidationFailed))] public static partial void MiddlewareAntiforgeryValidationFailed(ILogger logger); - [LoggerMessage(3, LogLevel.Debug, "The antiforgery middleware already succeeded to validate the current token.", EventName = "MiddlewareAntiforgeryValidationSucceeded")] + [LoggerMessage(3, LogLevel.Debug, "The antiforgery middleware already succeeded to validate the current token.", EventName = nameof(MiddlewareAntiforgeryValidationSucceeded))] public static partial void MiddlewareAntiforgeryValidationSucceeded(ILogger logger); - [LoggerMessage(4, LogLevel.Debug, "The endpoint disabled antiforgery token validation.", EventName = "EndpointAntiforgeryValidationDisabled")] + [LoggerMessage(4, LogLevel.Debug, "The endpoint disabled antiforgery token validation.", EventName = nameof(EndpointAntiforgeryValidationDisabled))] public static partial void EndpointAntiforgeryValidationDisabled(ILogger logger); - [LoggerMessage(5, LogLevel.Information, "Antiforgery token validation failed for the current request.", EventName = "EndpointAntiforgeryValidationFailed")] + [LoggerMessage(5, LogLevel.Information, "Antiforgery token validation failed for the current request.", EventName = nameof(EndpointAntiforgeryValidationFailed))] public static partial void EndpointAntiforgeryValidationFailed(ILogger logger); - [LoggerMessage(6, LogLevel.Debug, "Antiforgery token validation succeeded for the current request.", EventName = "EndpointAntiforgeryValidationSucceeded")] + [LoggerMessage(6, LogLevel.Debug, "Antiforgery token validation succeeded for the current request.", EventName = nameof(EndpointAntiforgeryValidationSucceeded))] public static partial void EndpointAntiforgeryValidationSucceeded(ILogger logger); + + [LoggerMessage(7, LogLevel.Debug, "Error handling in progress. Interactive components are not enabled.", EventName = nameof(InteractivityDisabledForErrorHandling))] + public static partial void InteractivityDisabledForErrorHandling(ILogger logger); } } diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index 6a2d399e3520..5b39693d1766 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -17,6 +17,11 @@ internal partial class EndpointHtmlRenderer protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode) { + if (_isHandlingErrors) + { + // Ignore the render mode boundary in error scenarios. + return componentActivator.CreateInstance(componentType); + } var closestRenderModeBoundary = parentComponentId.HasValue ? GetClosestRenderModeBoundary(parentComponentId.Value) : null; diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index 31ef21b0ad57..ea43f027cc82 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -19,9 +19,11 @@ internal partial class EndpointHtmlRenderer private TextWriter? _streamingUpdatesWriter; private HashSet? _visitedComponentIdsInCurrentStreamingBatch; private string? _ssrFramingCommentMarkup; + 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(); diff --git a/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs b/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs index 26fcb9cd18be..8f22818c7a27 100644 --- a/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs +++ b/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Text; using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; @@ -46,7 +47,8 @@ private static Task RenderComponentToResponse( var endpointHtmlRenderer = httpContext.RequestServices.GetRequiredService(); return endpointHtmlRenderer.Dispatcher.InvokeAsync(async () => { - endpointHtmlRenderer.InitializeStreamingRenderingFraming(httpContext); + var isErrorHandler = httpContext.Features.Get() is not null; + endpointHtmlRenderer.InitializeStreamingRenderingFraming(httpContext, isErrorHandler); EndpointHtmlRenderer.MarkAsAllowingEnhancedNavigation(httpContext); // We could pool these dictionary instances if we wanted, and possibly even the ParameterView 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/test/E2ETest/ServerRenderingTests/ErrorHandlingTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/ErrorHandlingTest.cs new file mode 100644 index 000000000000..219e7acfd4cf --- /dev/null +++ b/src/Components/test/E2ETest/ServerRenderingTests/ErrorHandlingTest.cs @@ -0,0 +1,45 @@ +// 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 async Task RendersExceptionFromComponent() + { + GoTo("Throws?suppress-autostart=true"); + + 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)); + Browser.Equal("False", () => Browser.FindElement(By.Id("is-interactive-server")).Text); + Browser.Click(By.Id("call-blazor-start")); + await Task.Delay(3000); + Browser.Exists(By.Id("blazor-started")); + Browser.Equal("False", () => Browser.FindElement(By.Id("is-interactive-server")).Text); + } + + private void GoTo(string relativePath) + { + Navigate($"{ServerPathBase}/{relativePath}"); + } +} 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)); } } 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/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index 91e64c581eb7..c0f1ba6cc0eb 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -65,6 +65,11 @@ if (useLongWebAssemblyTimeout) { Blazor._internal.loadWebAssemblyQuicklyTimeout = 10000000; } + }).then(() => { + const startedParagraph = document.createElement('p'); + startedParagraph.id = 'blazor-started'; + startedParagraph.style = 'display: none;'; + document.body.appendChild(startedParagraph); }); } 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..879417ae48e3 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Error.razor @@ -0,0 +1,37 @@ +@page "/Error" +@layout ErrorLayout +@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; } + + private string? RequestId { get; set; } + private 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/ErrorLayout.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ErrorLayout.razor new file mode 100644 index 000000000000..a14e57aaee92 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ErrorLayout.razor @@ -0,0 +1,7 @@ +@inherits Microsoft.AspNetCore.Components.LayoutComponentBase + + + +
+ @Body +
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 6256709900ba..fafeec524e72 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 createScopeForErrors) + { + ArgumentNullException.ThrowIfNull(app); + + return app.UseExceptionHandler(new ExceptionHandlerOptions + { + ExceptionHandlingPath = new PathString(errorHandlingPath), + CreateScopeForErrors = createScopeForErrors + }); + } + /// /// 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..e8f40e9b3fab 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.CreateScopeForErrors ? context.RequestServices : null; + using var scope = _options.CreateScopeForErrors ? 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..1b322019211d 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 when re-executing the request to handle an error. + /// + /// The default value is . + public bool CreateScopeForErrors { 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..f82528f7d333 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.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 +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! 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..ee6f5865a0c1 --- /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(); diff --git a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json index 6b982d80f7b2..340a30831f8a 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", @@ -631,6 +635,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", "Components/Routes.razor", @@ -665,6 +670,7 @@ "{ProjectName}.Client/Layout/NavMenu.razor", "{ProjectName}.Client/Layout/NavMenu.razor.css", "{ProjectName}.Client/Pages/Counter.razor", + "{ProjectName}.Client/Pages/Error.razor", "{ProjectName}.Client/Pages/Home.razor", "{ProjectName}.Client/Pages/Weather.razor", "{ProjectName}.Client/Program.cs", @@ -697,6 +703,7 @@ "{ProjectName}.Client/Layout/NavMenu.razor", "{ProjectName}.Client/Layout/NavMenu.razor.css", "{ProjectName}.Client/Pages/Counter.razor", + "{ProjectName}.Client/Pages/Error.razor", "{ProjectName}.Client/Pages/Home.razor", "{ProjectName}.Client/Pages/Weather.razor", "{ProjectName}.Client/Program.cs", @@ -722,6 +729,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 +749,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 +766,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 +794,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",