From f0784284cfb64d2e7022b57c6412965973cdf781 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 9 Jul 2025 17:25:10 +0200 Subject: [PATCH 1/3] SSR streaming not started with custom router. --- .../Components/src/NavigationManager.cs | 4 +- .../Components/src/PublicAPI.Unshipped.txt | 5 +- .../src/Routing/NotFoundEventArgs.cs | 13 +- .../Components/src/Routing/Router.cs | 7 +- .../src/RazorComponentEndpointInvoker.cs | 11 ++ .../EndpointHtmlRenderer.EventDispatch.cs | 2 +- .../src/Rendering/EndpointHtmlRenderer.cs | 2 + .../test/EndpointHtmlRendererTest.cs | 2 +- .../NoInteractivityTest.cs | 34 ++++ .../RazorComponents/App.razor | 31 +++- .../RazorComponents/CustomRouter.cs | 171 ++++++++++++++++++ 11 files changed, 256 insertions(+), 26 deletions(-) create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/CustomRouter.cs diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index 1900b91c629b..ff14da1adbb9 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -61,7 +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; + private readonly NotFoundEventArgs _notFoundEventArgs = new(); /// /// Gets or sets the current base URI. The is always represented as an absolute URI in string form with trailing slash. @@ -211,7 +211,7 @@ private void NotFoundCore() } else { - _notFound.Invoke(this, new NotFoundEventArgs(NotFoundPageRoute)); + _notFound.Invoke(this, _notFoundEventArgs); } } diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index dc07b8afddac..b69167202fee 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -8,8 +8,9 @@ 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(string! url) -> void -Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.Path.get -> string! +Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.NotFoundEventArgs() -> void +Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.Path.get -> string? +Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.Path.set -> void 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 e1e81e5cfc82..9263ed62a3ab 100644 --- a/src/Components/Components/src/Routing/NotFoundEventArgs.cs +++ b/src/Components/Components/src/Routing/NotFoundEventArgs.cs @@ -9,16 +9,7 @@ namespace Microsoft.AspNetCore.Components.Routing; public sealed class NotFoundEventArgs : EventArgs { /// - /// Gets the path of NotFoundPage. + /// Gets the path of NotFoundPage. If the path is set, it indicates that the router has handled the rendering of the NotFound contents. /// - public string Path { get; } - - /// - /// Initializes a new instance of . - /// - public NotFoundEventArgs(string url) - { - Path = url; - } - + public string? Path { get; set; } } diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index eedff373f656..667583389f86 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -29,6 +29,7 @@ static readonly IReadOnlyDictionary _emptyParametersDictionary string _locationAbsolute; bool _navigationInterceptionEnabled; ILogger _logger; + string _notFoundPageRoute; private string _updateScrollPositionForHashLastLocation; private bool _updateScrollPositionForHash; @@ -159,7 +160,7 @@ public async Task SetParametersAsync(ParameterView parameters) var routeAttribute = (RouteAttribute)routeAttributes[0]; if (routeAttribute.Template != null) { - NavigationManager.NotFoundPageRoute = routeAttribute.Template; + _notFoundPageRoute = routeAttribute.Template; } } @@ -381,12 +382,14 @@ private void OnLocationChanged(object sender, LocationChangedEventArgs args) } } - private void OnNotFound(object sender, EventArgs args) + private void OnNotFound(object sender, NotFoundEventArgs args) { if (_renderHandle.IsInitialized) { Log.DisplayingNotFound(_logger); RenderNotFound(); + // setting the path signals to the endpoint renderer that router handled rendering + args.Path = _notFoundPageRoute ?? "not-found-handled-by-router"; } } diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 8e5338f54788..ee9385c6cc1a 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -168,6 +168,17 @@ await _renderer.InitializeStandardComponentServicesAsync( componentStateHtmlContent.WriteTo(bufferWriter, HtmlEncoder.Default); } + if (context.Response.StatusCode == StatusCodes.Status404NotFound && + !isReExecuted && + string.IsNullOrEmpty(_renderer.NotFoundEventArgs?.Path)) + { + // Router did not handle the NotFound event, otherwise this would not be empty. + // Don't flush the response if we have an unhandled 404 rendering + // This will allow the StatusCodePages middleware to re-execute the request + context.Response.ContentType = null; + return; + } + // 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 // response as part of the Dispose which has a perf impact. diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index cdf17e376a00..f0060c25914f 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -109,7 +109,7 @@ internal async Task SetNotFoundResponseAsync(string baseUri, NotFoundEventArgs a private string GetNotFoundUrl(string baseUri, NotFoundEventArgs args) { - string path = args.Path; + string? path = args.Path; if (string.IsNullOrEmpty(path)) { var pathFormat = _httpContext.Items[nameof(StatusCodePagesOptions)] as string; diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index f5d0699e1efe..7214516348b0 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -63,6 +63,7 @@ public EndpointHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory log } internal HttpContext? HttpContext => _httpContext; + internal NotFoundEventArgs? NotFoundEventArgs { get; set; } internal void SetHttpContext(HttpContext httpContext) { @@ -87,6 +88,7 @@ internal async Task InitializeStandardComponentServicesAsync( navigationManager?.OnNotFound += (sender, args) => { + NotFoundEventArgs = args; _ = GetErrorHandledTask(SetNotFoundResponseAsync(navigationManager.BaseUri, args)); }; diff --git a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs index 3c27365169cb..caa5d1c14c1c 100644 --- a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs +++ b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs @@ -944,7 +944,7 @@ public async Task Renderer_WhenNoNotFoundPathProvided_Throws() httpContext.Items[nameof(StatusCodePagesOptions)] = null; // simulate missing re-execution route var exception = await Assert.ThrowsAsync(async () => - await renderer.SetNotFoundResponseAsync(httpContext, new NotFoundEventArgs("")) + await renderer.SetNotFoundResponseAsync(httpContext, new NotFoundEventArgs()) ); string expectedError = "The NotFoundPage route must be specified or re-execution middleware has to be set to render NotFoundPage when the response has started."; diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index bbeb1ad4d9bb..c90ea78845e2 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -117,6 +117,16 @@ public void BrowserNavigationToNotExistingPathReExecutesTo404(bool streaming) private void AssertReExecutionPageRendered() => Browser.Equal("Welcome On Page Re-executed After Not Found Event", () => Browser.Exists(By.Id("test-info")).Text); + private void AssertBrowserDefaultNotFoundViewRendered() + { + var mainMessage = Browser.FindElement(By.Id("main-message")); + + Browser.True( + () => mainMessage.FindElement(By.CssSelector("p")).Text + .Contains("No webpage was found for the web address:", StringComparison.OrdinalIgnoreCase) + ); + } + private void AssertNotFoundPageRendered() { Browser.Equal("Welcome On Custom Not Found Page", () => Browser.FindElement(By.Id("test-info")).Text); @@ -152,6 +162,30 @@ public void NotFoundSetOnInitialization_ResponseNotStarted_SSR(bool hasReExecuti AssertUrlNotChanged(testUrl); } + [Theory] + [InlineData(true)] + [InlineData(false)] + // our custom router does not support NotFoundPage nor NotFound fragment to simulate most probable custom router behavior + public void NotFoundSetOnInitialization_ResponseNotStarted_CustomRouter_SSR(bool hasReExecutionMiddleware) + { + string reexecution = hasReExecutionMiddleware ? "/reexecution" : ""; + string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr?useCustomRouter=true"; + Navigate(testUrl); + + if (hasReExecutionMiddleware) + { + AssertReExecutionPageRendered(); + } + else + { + // Apps that don't support re-execution and don't have blazor's router, + // cannot render custom NotFound contents. + // The browser will display default 404 page. + AssertBrowserDefaultNotFoundViewRendered(); + } + AssertUrlNotChanged(testUrl); + } + [Theory] [InlineData(true, true)] [InlineData(true, false)] diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index 2219da3955d2..457fcb518f4c 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -1,12 +1,17 @@ @using Components.TestServer.RazorComponents.Pages.Forms @using Components.WasmMinimal.Pages.NotFound @using TestContentPackage.NotFound +@using Components.TestServer.RazorComponents @code { [Parameter] [SupplyParameterFromQuery(Name = "useCustomNotFoundPage")] public string? UseCustomNotFoundPage { get; set; } + [Parameter] + [SupplyParameterFromQuery(Name = "useCustomRouter")] + public string? UseCustomRouter { get; set; } + private Type? NotFoundPageType { get; set; } protected override void OnParametersSet() @@ -30,13 +35,25 @@ - - - - - -

There's nothing here

-
+ @if(string.Equals(UseCustomRouter, "true", StringComparison.OrdinalIgnoreCase)) + { + + + + + + + } + else + { + + + + + +

There's nothing here

+
+ }