Skip to content

Support NotFound content rendering for a custom Router #62635

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jul 17, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
SSR streaming not started with custom router.
  • Loading branch information
ilonatommy committed Jul 9, 2025
commit f0784284cfb64d2e7022b57c6412965973cdf781
4 changes: 2 additions & 2 deletions src/Components/Components/src/NavigationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public event EventHandler<NotFoundEventArgs> 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();

/// <summary>
/// Gets or sets the current base URI. The <see cref="BaseUri" /> is always represented as an absolute URI in string form with trailing slash.
Expand Down Expand Up @@ -211,7 +211,7 @@ private void NotFoundCore()
}
else
{
_notFound.Invoke(this, new NotFoundEventArgs(NotFoundPageRoute));
_notFound.Invoke(this, _notFoundEventArgs);
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<string!, System.Threading.Tasks.Task!>! 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<Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager!>! logger, System.IServiceProvider! serviceProvider) -> void
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.SetPlatformRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void
Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions
Expand Down
13 changes: 2 additions & 11 deletions src/Components/Components/src/Routing/NotFoundEventArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,7 @@ namespace Microsoft.AspNetCore.Components.Routing;
public sealed class NotFoundEventArgs : EventArgs
{
/// <summary>
/// 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.
/// </summary>
public string Path { get; }

/// <summary>
/// Initializes a new instance of <see cref="NotFoundEventArgs" />.
/// </summary>
public NotFoundEventArgs(string url)
{
Path = url;
}

public string? Path { get; set; }
}
7 changes: 5 additions & 2 deletions src/Components/Components/src/Routing/Router.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ static readonly IReadOnlyDictionary<string, object> _emptyParametersDictionary
string _locationAbsolute;
bool _navigationInterceptionEnabled;
ILogger<Router> _logger;
string _notFoundPageRoute;

private string _updateScrollPositionForHashLastLocation;
private bool _updateScrollPositionForHash;
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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";
}
}

Expand Down
11 changes: 11 additions & 0 deletions src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public EndpointHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory log
}

internal HttpContext? HttpContext => _httpContext;
internal NotFoundEventArgs? NotFoundEventArgs { get; set; }

internal void SetHttpContext(HttpContext httpContext)
{
Expand All @@ -87,6 +88,7 @@ internal async Task InitializeStandardComponentServicesAsync(

navigationManager?.OnNotFound += (sender, args) =>
{
NotFoundEventArgs = args;
_ = GetErrorHandledTask(SetNotFoundResponseAsync(navigationManager.BaseUri, args));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<InvalidOperationException>(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.";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)]
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -30,13 +35,25 @@
<HeadOutlet />
</head>
<body>
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="new[] { typeof(TestContentPackage.NotFound.NotFoundPage).Assembly }" NotFoundPage="NotFoundPageType">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
<FocusOnNavigate RouteData="@routeData" Selector="[data-focus-on-navigate]" />
</Found>
<NotFound><p id="not-found-fragment">There's nothing here</p></NotFound>
</Router>
@if(string.Equals(UseCustomRouter, "true", StringComparison.OrdinalIgnoreCase))
{
<CustomRouter AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
<FocusOnNavigate RouteData="@routeData" Selector="[data-focus-on-navigate]" />
</Found>
</CustomRouter>
}
else
{
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="new[] { typeof(TestContentPackage.NotFound.NotFoundPage).Assembly }" NotFoundPage="NotFoundPageType">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
<FocusOnNavigate RouteData="@routeData" Selector="[data-focus-on-navigate]" />
</Found>
<NotFound><p id="not-found-fragment">There's nothing here</p></NotFound>
</Router>
}
<script>
// This script must come before blazor.web.js to test that
// the framework does the right thing when an element is already focused.
Expand Down
Loading
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy