diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index 25d6da65b94e..1900b91c629b 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -54,8 +54,6 @@ public event EventHandler OnNotFound private EventHandler? _notFound; - private static readonly NotFoundEventArgs _notFoundEventArgs = new NotFoundEventArgs(); - // For the baseUri it's worth storing as a System.Uri so we can do operations // on that type. System.Uri gives us access to the original string anyway. private Uri? _baseUri; @@ -63,6 +61,7 @@ public event EventHandler OnNotFound // The URI. Always represented an absolute URI. private string? _uri; private bool _isInitialized; + internal string NotFoundPageRoute { get; set; } = string.Empty; /// /// Gets or sets the current base URI. The is always represented as an absolute URI in string form with trailing slash. @@ -212,7 +211,7 @@ private void NotFoundCore() } else { - _notFound.Invoke(this, _notFoundEventArgs); + _notFound.Invoke(this, new NotFoundEventArgs(NotFoundPageRoute)); } } diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 5eb52d2c330e..07e51aca6bd3 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -6,7 +6,8 @@ Microsoft.AspNetCore.Components.NavigationManager.OnNotFound -> System.EventHand Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, System.Func! onNavigateTo) -> void Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs -Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.NotFoundEventArgs() -> void +Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.NotFoundEventArgs(string! url) -> void +Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.Path.get -> string! Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.ComponentStatePersistenceManager(Microsoft.Extensions.Logging.ILogger! logger, System.IServiceProvider! serviceProvider) -> void Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.SetPlatformRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions diff --git a/src/Components/Components/src/Routing/NotFoundEventArgs.cs b/src/Components/Components/src/Routing/NotFoundEventArgs.cs index 637bfd442b70..e1e81e5cfc82 100644 --- a/src/Components/Components/src/Routing/NotFoundEventArgs.cs +++ b/src/Components/Components/src/Routing/NotFoundEventArgs.cs @@ -8,9 +8,17 @@ namespace Microsoft.AspNetCore.Components.Routing; /// public sealed class NotFoundEventArgs : EventArgs { + /// + /// Gets the path of NotFoundPage. + /// + public string Path { get; } + /// /// Initializes a new instance of . /// - public NotFoundEventArgs() - { } + public NotFoundEventArgs(string url) + { + Path = url; + } + } diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 7a73ead53ea9..eedff373f656 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -155,6 +155,12 @@ public async Task SetParametersAsync(ParameterView parameters) throw new InvalidOperationException($"The type {NotFoundPage.FullName} " + $"does not have a {typeof(RouteAttribute).FullName} applied to it."); } + + var routeAttribute = (RouteAttribute)routeAttributes[0]; + if (routeAttribute.Template != null) + { + NavigationManager.NotFoundPageRoute = routeAttribute.Template; + } } if (!_onNavigateCalled) diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index a52f6e274410..8e5338f54788 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -111,13 +111,6 @@ await _renderer.InitializeStandardComponentServicesAsync( ParameterView.Empty, waitForQuiescence: result.IsPost || isErrorHandlerOrReExecuted); - bool avoidStartingResponse = hasStatusCodePage && !isReExecuted && context.Response.StatusCode == StatusCodes.Status404NotFound; - if (avoidStartingResponse) - { - // the request is going to be re-executed, we should avoid writing to the response - return; - } - Task quiesceTask; if (!result.IsPost) { diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index 6af020fc847a..2ef85e2513b5 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -1,9 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Components.Endpoints.Rendering; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; @@ -77,21 +79,26 @@ private Task ReturnErrorResponse(string detailedMessage) : Task.CompletedTask; } - private async Task SetNotFoundResponseAsync(string baseUri) + internal async Task SetNotFoundResponseAsync(string baseUri, NotFoundEventArgs args) { - if (_httpContext.Response.HasStarted) + if (_httpContext.Response.HasStarted || + // POST waits for quiescence -> rendering the NotFoundPage would be queued for the next batch + // but we want to send the signal to the renderer to stop rendering future batches -> use client rendering + string.Equals(_httpContext.Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { + if (string.IsNullOrEmpty(_notFoundUrl)) + { + _notFoundUrl = GetNotFoundUrl(baseUri, args); + } var defaultBufferSize = 16 * 1024; await using var writer = new HttpResponseStreamWriter(_httpContext.Response.Body, Encoding.UTF8, defaultBufferSize, ArrayPool.Shared, ArrayPool.Shared); using var bufferWriter = new BufferedTextWriter(writer); - var notFoundUri = $"{baseUri}not-found"; - HandleNavigationAfterResponseStarted(bufferWriter, _httpContext, notFoundUri); + HandleNotFoundAfterResponseStarted(bufferWriter, _httpContext, _notFoundUrl); await bufferWriter.FlushAsync(); } else { _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; - _httpContext.Response.ContentType = null; } // When the application triggers a NotFound event, we continue rendering the current batch. @@ -100,6 +107,22 @@ private async Task SetNotFoundResponseAsync(string baseUri) SignalRendererToFinishRendering(); } + private string GetNotFoundUrl(string baseUri, NotFoundEventArgs args) + { + string path = args.Path; + if (string.IsNullOrEmpty(path)) + { + var pathFormat = _httpContext.Items[nameof(StatusCodePagesOptions)] as string; + if (string.IsNullOrEmpty(pathFormat)) + { + throw new InvalidOperationException("The NotFoundPage route must be specified or re-execution middleware has to be set to render NotFoundPage when the response has started."); + } + + path = pathFormat; + } + return $"{baseUri}{path.TrimStart('/')}"; + } + private async Task OnNavigateTo(string uri) { if (_httpContext.Response.HasStarted) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index c17f7cd53555..e78a938fc9d6 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -226,10 +226,20 @@ private static void HandleExceptionAfterResponseStarted(HttpContext httpContext, writer.Write(""); } + private static void HandleNotFoundAfterResponseStarted(TextWriter writer, HttpContext httpContext, string notFoundUrl) + { + writer.Write("