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<NotFoundEventArgs> OnNotFound
 
     private EventHandler<NotFoundEventArgs>? _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<NotFoundEventArgs> OnNotFound
     // The URI. Always represented an absolute URI.
     private string? _uri;
     private bool _isInitialized;
+    internal string NotFoundPageRoute { get; set; } = string.Empty;
 
     /// <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.
@@ -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<string!, System.Threading.Tasks.Task!>! 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<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
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;
 /// </summary>
 public sealed class NotFoundEventArgs : EventArgs
 {
+    /// <summary>
+    /// Gets the path of NotFoundPage.
+    /// </summary>
+    public string Path { get; }
+
     /// <summary>
     /// Initializes a new instance of <see cref="NotFoundEventArgs" />.
     /// </summary>
-    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..cdf17e376a00 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
+            HttpMethods.IsPost(_httpContext.Request.Method))
         {
+            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<byte>.Shared, ArrayPool<char>.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..926aa42fe8d8 100644
--- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs
+++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs
@@ -226,16 +226,27 @@ private static void HandleExceptionAfterResponseStarted(HttpContext httpContext,
         writer.Write("</template><blazor-ssr-end></blazor-ssr-end></blazor-ssr>");
     }
 
+    private static void HandleNotFoundAfterResponseStarted(TextWriter writer, HttpContext httpContext, string notFoundUrl)
+    {
+        writer.Write("<blazor-ssr><template type=\"not-found\"");
+        WriteResponseTemplate(writer, httpContext, notFoundUrl, useEnhancedNav: true);
+    }
+
     private static void HandleNavigationAfterResponseStarted(TextWriter writer, HttpContext httpContext, string destinationUrl)
     {
         writer.Write("<blazor-ssr><template type=\"redirection\"");
+        bool useEnhancedNav = IsProgressivelyEnhancedNavigation(httpContext.Request);
+        WriteResponseTemplate(writer, httpContext, destinationUrl, useEnhancedNav);
+    }
 
-        if (string.Equals(httpContext.Request.Method, "POST", StringComparison.OrdinalIgnoreCase))
+    private static void WriteResponseTemplate(TextWriter writer, HttpContext httpContext, string destinationUrl, bool useEnhancedNav)
+    {
+        if (HttpMethods.IsPost(httpContext.Request.Method))
         {
             writer.Write(" from=\"form-post\"");
         }
 
-        if (IsProgressivelyEnhancedNavigation(httpContext.Request))
+        if (useEnhancedNav)
         {
             writer.Write(" enhanced=\"true\"");
         }
diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs
index 33e52fa0ed57..f5d0699e1efe 100644
--- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs
+++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs
@@ -52,6 +52,8 @@ internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrer
     // wait for the non-streaming tasks (these ones), then start streaming until full quiescence.
     private readonly List<Task> _nonStreamingPendingTasks = new();
 
+    private string _notFoundUrl = string.Empty;
+
     public EndpointHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory)
         : base(serviceProvider, loggerFactory)
     {
@@ -62,7 +64,7 @@ public EndpointHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory log
 
     internal HttpContext? HttpContext => _httpContext;
 
-    private void SetHttpContext(HttpContext httpContext)
+    internal void SetHttpContext(HttpContext httpContext)
     {
         if (_httpContext is null)
         {
@@ -83,10 +85,10 @@ internal async Task InitializeStandardComponentServicesAsync(
         var navigationManager = httpContext.RequestServices.GetRequiredService<NavigationManager>();
         ((IHostEnvironmentNavigationManager)navigationManager)?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request), OnNavigateTo);
 
-        if (navigationManager != null)
+        navigationManager?.OnNotFound += (sender, args) =>
         {
-            navigationManager.OnNotFound += async (sender, args) => await SetNotFoundResponseAsync(navigationManager.BaseUri);
-        }
+            _ = GetErrorHandledTask(SetNotFoundResponseAsync(navigationManager.BaseUri, args));
+        };
 
         var authenticationStateProvider = httpContext.RequestServices.GetService<AuthenticationStateProvider>();
         if (authenticationStateProvider is IHostEnvironmentAuthenticationStateProvider hostEnvironmentAuthenticationStateProvider)
diff --git a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs
index ac787b76bfe5..23b4b87d0b2f 100644
--- a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs
+++ b/src/Components/Endpoints/test/EndpointHtmlRendererTest.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 System;
 using System.Text.Encodings.Web;
 using System.Text.Json;
 using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Components.Endpoints.Forms;
 using Microsoft.AspNetCore.Components.Endpoints.Tests.TestComponents;
 using Microsoft.AspNetCore.Components.Forms;
@@ -12,6 +14,7 @@
 using Microsoft.AspNetCore.Components.Reflection;
 using Microsoft.AspNetCore.Components.Rendering;
 using Microsoft.AspNetCore.Components.RenderTree;
+using Microsoft.AspNetCore.Components.Routing;
 using Microsoft.AspNetCore.Components.Test.Helpers;
 using Microsoft.AspNetCore.Components.Web;
 using Microsoft.AspNetCore.DataProtection;
@@ -934,6 +937,26 @@ await renderer.PrerenderComponentAsync(
         Assert.Equal("http://localhost/redirect", ctx.Response.Headers.Location);
     }
 
+    [Fact]
+    public async Task Renderer_WhenNoNotFoundPathProvided_Throws()
+    {
+        // Arrange
+        var httpContext = GetHttpContext();
+        var responseMock = new Mock<IHttpResponseFeature>();
+        responseMock.Setup(r => r.HasStarted).Returns(true);
+        responseMock.Setup(r => r.Headers).Returns(new HeaderDictionary());
+        httpContext.Features.Set(responseMock.Object);
+        var renderer = GetEndpointHtmlRenderer();
+        httpContext.Items[nameof(StatusCodePagesOptions)] = null; // simulate missing re-execution route
+
+        var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
+            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.";
+
+        Assert.Equal(expectedError, exception.Message);
+    }
+
     [Fact]
     public async Task CanRender_AsyncComponent()
     {
@@ -1802,6 +1825,12 @@ protected override void ProcessPendingRender()
             _rendererIsStopped = true;
             base.SignalRendererToFinishRendering();
         }
+
+        public async Task SetNotFoundResponseAsync(HttpContext httpContext, NotFoundEventArgs args)
+        {
+            SetHttpContext(httpContext);
+            await SetNotFoundResponseAsync(httpContext.Request.PathBase, args);
+        }
     }
 
     private HttpContext GetHttpContext(HttpContext context = null)
diff --git a/src/Components/Web.JS/src/Rendering/StreamingRendering.ts b/src/Components/Web.JS/src/Rendering/StreamingRendering.ts
index 06c650c2f99c..7ac02952068e 100644
--- a/src/Components/Web.JS/src/Rendering/StreamingRendering.ts
+++ b/src/Components/Web.JS/src/Rendering/StreamingRendering.ts
@@ -45,35 +45,16 @@ class BlazorStreamingUpdate extends HTMLElement {
             insertStreamingContentIntoDocument(componentId, node.content);
           }
         } else {
+          const isEnhancedNav = node.getAttribute('enhanced') === 'true';
           switch (node.getAttribute('type')) {
             case 'redirection':
-              // We use 'replace' here because it's closest to the non-progressively-enhanced behavior, and will make the most sense
-              // if the async delay was very short, as the user would not perceive having been on the intermediate page.
-              const destinationUrl = toAbsoluteUri(node.content.textContent!);
-              const isFormPost = node.getAttribute('from') === 'form-post';
-              const isEnhancedNav = node.getAttribute('enhanced') === 'true';
-              if (isEnhancedNav && isWithinBaseUriSpace(destinationUrl)) {
-                // At this point the destinationUrl might be an opaque URL so we don't know whether it's internal/external or
-                // whether it's even going to the same URL we're currently on. So we don't know how to update the history.
-                // Defer that until the redirection is resolved by performEnhancedPageLoad.
-                const treatAsRedirectionFromMethod = isFormPost ? 'post' : 'get';
-                const fetchOptions = undefined;
-                performEnhancedPageLoad(destinationUrl, /* interceptedLink */ false, fetchOptions, treatAsRedirectionFromMethod);
-              } else {
-                if (isFormPost) {
-                  // The URL is not yet updated. Push a whole new entry so that 'back' goes back to the pre-redirection location.
-                  // WARNING: The following check to avoid duplicating history entries won't work if the redirection is to an opaque URL.
-                  // We could change the server-side logic to return URLs in plaintext if they match the current request URL already,
-                  // but it's arguably easier to understand that history non-duplication only works for enhanced nav, which is also the
-                  // case for non-streaming responses.
-                  if (destinationUrl !== location.href) {
-                    location.assign(destinationUrl);
-                  }
-                } else {
-                  // The URL was already updated on the original link click. Replace so that 'back' goes to the pre-redirection location.
-                  location.replace(destinationUrl);
-                }
-              }
+              redirect(node, true, isEnhancedNav);
+              break;
+            case 'not-found':
+              // not-found template has enhanced nav set to true by default,
+              // check for the options to avoid overriding user settings
+              const useEnhancedNav = isEnhancedNav && enableDomPreservation;
+              redirect(node, false, useEnhancedNav);
               break;
             case 'error':
               // This is kind of brutal but matches what happens without progressive enhancement
@@ -86,6 +67,35 @@ class BlazorStreamingUpdate extends HTMLElement {
   }
 }
 
+function redirect(node: HTMLTemplateElement, changeUrl: boolean, isEnhancedNav: boolean): void {
+  // We use 'replace' here because it's closest to the non-progressively-enhanced behavior, and will make the most sense
+ // if the async delay was very short, as the user would not perceive having been on the intermediate page.
+ const destinationUrl = toAbsoluteUri(node.content.textContent!);
+ const isFormPost = node.getAttribute('from') === 'form-post';
+ if (isEnhancedNav && isWithinBaseUriSpace(destinationUrl)) {
+   // At this point the destinationUrl might be an opaque URL so we don't know whether it's internal/external or
+   // whether it's even going to the same URL we're currently on. So we don't know how to update the history.
+   // Defer that until the redirection is resolved by performEnhancedPageLoad.
+   const treatAsRedirectionFromMethod = isFormPost ? 'post' : 'get';
+   const fetchOptions = undefined;
+   performEnhancedPageLoad(destinationUrl, /* interceptedLink */ false, fetchOptions, treatAsRedirectionFromMethod, changeUrl);
+ } else {
+   if (isFormPost) {
+     // The URL is not yet updated. Push a whole new entry so that 'back' goes back to the pre-redirection location.
+     // WARNING: The following check to avoid duplicating history entries won't work if the redirection is to an opaque URL.
+     // We could change the server-side logic to return URLs in plaintext if they match the current request URL already,
+     // but it's arguably easier to understand that history non-duplication only works for enhanced nav, which is also the
+     // case for non-streaming responses.
+     if (destinationUrl !== location.href) {
+       location.assign(destinationUrl);
+     }
+   } else {
+     // The URL was already updated on the original link click. Replace so that 'back' goes to the pre-redirection location.
+     location.replace(destinationUrl);
+   }
+  }
+}
+
 function insertStreamingContentIntoDocument(componentIdAsString: string, docFrag: DocumentFragment): void {
   const markers = findStreamingMarkers(componentIdAsString);
   if (markers) {
diff --git a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
index 9965f2b727e2..e029c77178dd 100644
--- a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
+++ b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
@@ -192,7 +192,7 @@ function onDocumentSubmit(event: SubmitEvent) {
   }
 }
 
-export async function performEnhancedPageLoad(internalDestinationHref: string, interceptedLink: boolean, fetchOptions?: RequestInit, treatAsRedirectionFromMethod?: 'get' | 'post') {
+export async function performEnhancedPageLoad(internalDestinationHref: string, interceptedLink: boolean, fetchOptions?: RequestInit, treatAsRedirectionFromMethod?: 'get' | 'post', changeUrl: boolean = true) {
   performingEnhancedPageLoad = true;
 
   // First, stop any preceding enhanced page load
@@ -257,7 +257,7 @@ export async function performEnhancedPageLoad(internalDestinationHref: string, i
       // For 301/302/etc redirections to internal URLs, the browser will already have followed the chain of redirections
       // to the end, and given us the final content. We do still need to update the current URL to match the final location,
       // then let the rest of enhanced nav logic run to patch the new content into the DOM.
-      if (response.redirected || treatAsRedirectionFromMethod) {
+      if (changeUrl && (response.redirected || treatAsRedirectionFromMethod)) {
         const treatAsGet = treatAsRedirectionFromMethod ? (treatAsRedirectionFromMethod === 'get') : isGetRequest;
         if (treatAsGet) {
           // For gets, the intermediate (redirecting) URL is already in the address bar, so we have to use 'replace'
@@ -274,12 +274,12 @@ export async function performEnhancedPageLoad(internalDestinationHref: string, i
 
       // For enhanced nav redirecting to an external URL, we'll get a special Blazor-specific redirection command
       const externalRedirectionUrl = response.headers.get('blazor-enhanced-nav-redirect-location');
-      if (externalRedirectionUrl) {
+      if (changeUrl && externalRedirectionUrl) {
         location.replace(externalRedirectionUrl);
         return;
       }
 
-      if (!response.redirected && !isGetRequest && isSuccessResponse) {
+      if (changeUrl && !response.redirected && !isGetRequest && isSuccessResponse) {
         // If this is the result of a form post that didn't trigger a redirection.
         if (!isForSamePath(response.url, currentContentUrl)) {
           // In this case we don't want to push the currentContentUrl to the history stack because we don't know if this is a location
@@ -296,7 +296,9 @@ export async function performEnhancedPageLoad(internalDestinationHref: string, i
       }
 
       // Set the currentContentUrl to the location of the last completed navigation.
-      currentContentUrl = response.url;
+      if (changeUrl) {
+        currentContentUrl = response.url;
+      }
 
       const responseContentType = response.headers.get('content-type');
       if (responseContentType?.startsWith('text/html') && initialContent) {
diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs
index 35f7140f9170..e5a438283f84 100644
--- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs
+++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs
@@ -80,13 +80,6 @@ public void NavigatesWithoutInteractivityByRequestRedirection(bool controlFlowBy
         Browser.Equal("Routing test cases", () => Browser.Exists(By.Id("test-info")).Text);
     }
 
-    [Fact]
-    public void CanRenderNotFoundPageAfterStreamingStarted()
-    {
-        Navigate($"{ServerPathBase}/streaming-set-not-found");
-        Browser.Equal("Default Not Found Page", () => Browser.Title);
-    }
-
     [Theory]
     [InlineData(true)]
     [InlineData(false)]
@@ -94,7 +87,7 @@ public void ProgrammaticNavigationToNotExistingPathReExecutesTo404(bool streamin
     {
         string streamingPath = streaming ? "-streaming" : "";
         Navigate($"{ServerPathBase}/reexecution/redirection-not-found-ssr{streamingPath}?navigate-programmatically=true");
-        Assert404ReExecuted();
+        AssertReExecutionPageRendered();
     }
 
     [Theory]
@@ -105,7 +98,7 @@ public void LinkNavigationToNotExistingPathReExecutesTo404(bool streaming)
         string streamingPath = streaming ? "-streaming" : "";
         Navigate($"{ServerPathBase}/reexecution/redirection-not-found-ssr{streamingPath}");
         Browser.Click(By.Id("link-to-not-existing-page"));
-        Assert404ReExecuted();
+        AssertReExecutionPageRendered();
     }
 
     [Theory]
@@ -118,45 +111,134 @@ public void BrowserNavigationToNotExistingPathReExecutesTo404(bool streaming)
         // will not be activated, see configuration in Startup
         string streamingPath = streaming ? "-streaming" : "";
         Navigate($"{ServerPathBase}/reexecution/not-existing-page-ssr{streamingPath}");
-        Assert404ReExecuted();
+        AssertReExecutionPageRendered();
     }
 
-    private void Assert404ReExecuted() =>
+    private void AssertReExecutionPageRendered() =>
         Browser.Equal("Welcome On Page Re-executed After Not Found Event", () => Browser.Exists(By.Id("test-info")).Text);
 
+    private void AssertNotFoundPageRendered()
+    {
+        Browser.Equal("Welcome On Custom Not Found Page", () => Browser.FindElement(By.Id("test-info")).Text);
+        // custom page should have a custom layout
+        Browser.Equal("About", () => Browser.FindElement(By.Id("about-link")).Text);
+    }
+
+    private void AssertUrlNotChanged(string expectedUrl) =>
+        Browser.True(() => Browser.Url.Contains(expectedUrl), $"Expected URL to contain '{expectedUrl}', but found '{Browser.Url}'");
+
+    private void AssertUrlChanged(string urlPart) =>
+        Browser.False(() => Browser.Url.Contains(urlPart), $"Expected URL not to contain '{urlPart}', but found '{Browser.Url}'");
+
     [Theory]
-    [InlineData(true)]
-    [InlineData(false)]
-    public void CanRenderNotFoundPageNoStreaming(bool useCustomNotFoundPage)
+    [InlineData(true, true)]
+    [InlineData(true, false)]
+    [InlineData(false, true)]
+    [InlineData(false, false)]
+    public void NotFoundSetOnInitialization_ResponseNotStarted_SSR(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet)
     {
-        string query = useCustomNotFoundPage ? "&useCustomNotFoundPage=true" : "";
-        Navigate($"{ServerPathBase}/set-not-found?shouldSet=true{query}");
+        string reexecution = hasReExecutionMiddleware ? "/reexecution" : "";
+        string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr?useCustomNotFoundPage={hasCustomNotFoundPageSet}";
+        Navigate(testUrl);
 
-        if (useCustomNotFoundPage)
+        if (hasCustomNotFoundPageSet)
         {
-            var infoText = Browser.FindElement(By.Id("test-info")).Text;
-            Assert.Contains("Welcome On Custom Not Found Page", infoText);
-            // custom page should have a custom layout
-            var aboutLink = Browser.FindElement(By.Id("about-link")).Text;
-            Assert.Contains("About", aboutLink);
+            AssertNotFoundPageRendered();
         }
         else
         {
-            var bodyText = Browser.FindElement(By.TagName("body")).Text;
-            Assert.Contains("There's nothing here", bodyText);
+            AssertNotFoundFragmentRendered();
         }
+        AssertUrlNotChanged(testUrl);
     }
 
     [Theory]
-    [InlineData(true)]
-    [InlineData(false)]
-    public void CanRenderNotFoundPageWithStreaming(bool useCustomNotFoundPage)
+    [InlineData(true, true)]
+    [InlineData(true, false)]
+    [InlineData(false, true)]
+    [InlineData(false, false)]
+    public void NotFoundSetOnInitialization_ResponseStarted_SSR(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet)
+    {
+        string reexecution = hasReExecutionMiddleware ? "/reexecution" : "";
+        string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr-streaming?useCustomNotFoundPage={hasCustomNotFoundPageSet}";
+        Navigate(testUrl);
+        AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl);
+        AssertUrlNotChanged(testUrl);
+    }
+
+    [Theory]
+    [InlineData(true, true)]
+    [InlineData(true, false)]
+    [InlineData(false, true)]
+    public void NotFoundSetOnInitialization_ResponseStarted_EnhancedNavigationDisabled_SSR(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet)
     {
-        // when streaming started, we always render page under "not-found" path
-        string query = useCustomNotFoundPage ? "?useCustomNotFoundPage=true" : "";
-        Navigate($"{ServerPathBase}/streaming-set-not-found{query}");
+        EnhancedNavigationTestUtil.SuppressEnhancedNavigation(this, true, skipNavigation: true);
+        string reexecution = hasReExecutionMiddleware ? "/reexecution" : "";
+        string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr-streaming?useCustomNotFoundPage={hasCustomNotFoundPageSet}";
+        Navigate(testUrl);
+        AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl);
+        AssertUrlChanged(testUrl);
+    }
 
-        string expectedTitle = "Default Not Found Page";
-        Browser.Equal(expectedTitle, () => Browser.Title);
+    private void AssertNotFoundRendered_ResponseStarted_Or_POST(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet, string testUrl)
+    {
+        if (hasCustomNotFoundPageSet)
+        {
+            AssertNotFoundPageRendered();
+        }
+        else if (hasReExecutionMiddleware)
+        {
+            AssertReExecutionPageRendered();
+        }
+        else
+        {
+            // this throws an exception logged on the server
+            AssertNotFoundContentNotRendered();
+        }
+    }
+
+    [Theory]
+    [InlineData(true, true)]
+    [InlineData(true, false)]
+    [InlineData(false, true)]
+    [InlineData(false, false)]
+    public void NotFoundSetOnFormSubmit_ResponseNotStarted_SSR(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet)
+    {
+        string reexecution = hasReExecutionMiddleware ? "/reexecution" : "";
+        string testUrl = $"{ServerPathBase}{reexecution}/post-not-found-ssr-streaming?useCustomNotFoundPage={hasCustomNotFoundPageSet}";
+        Navigate(testUrl);
+        Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click();
+
+        AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl);
+        AssertUrlNotChanged(testUrl);
+    }
+
+    [Theory]
+    [InlineData(true, true)]
+    [InlineData(true, false)]
+    [InlineData(false, true)]
+    [InlineData(false, false)]
+    public void NotFoundSetOnFormSubmit_ResponseStarted_SSR(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet)
+    {
+        string reexecution = hasReExecutionMiddleware ? "/reexecution" : "";
+        string testUrl = $"{ServerPathBase}{reexecution}/post-not-found-ssr-streaming?useCustomNotFoundPage={hasCustomNotFoundPageSet}";
+        Navigate(testUrl);
+        Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click();
+
+        AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl);
+        AssertUrlNotChanged(testUrl);
+    }
+
+    private void AssertNotFoundFragmentRendered() =>
+        Browser.Equal("There's nothing here", () => Browser.FindElement(By.Id("not-found-fragment")).Text);
+
+    private void AssertNotFoundContentNotRendered() =>
+        Browser.Equal("Any content", () => Browser.FindElement(By.Id("test-info")).Text);
+
+    [Fact]
+    public void StatusCodePagesWithReExecution()
+    {
+        Navigate($"{ServerPathBase}/reexecution/trigger-404");
+        Browser.Equal("Re-executed page", () => Browser.Title);
     }
 }
diff --git a/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs
deleted file mode 100644
index 58ac90b39bbe..000000000000
--- a/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-// 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 Components.TestServer.RazorComponents;
-using Components.TestServer.RazorComponents.Pages.StreamingRendering;
-using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
-using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
-using Microsoft.AspNetCore.E2ETesting;
-using OpenQA.Selenium;
-using TestServer;
-using Xunit.Abstractions;
-
-namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests;
-
-public class StatusCodePagesTest(BrowserFixture browserFixture, BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>> serverFixture, ITestOutputHelper output)
-    : ServerTestBase<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>>>(browserFixture, serverFixture, output)
-{
-
-    [Theory]
-    [InlineData(false, false)]
-    [InlineData(true, false)]
-    [InlineData(true, true)]
-    public void StatusCodePagesWithReExecution(bool streaming, bool responseStarted)
-    {
-        string streamingPath = streaming ? "streaming-" : "";
-        Navigate($"{ServerPathBase}/reexecution/{streamingPath}set-not-found?responseStarted={responseStarted}");
-
-        // streaming when response started does not re-execute
-        string expectedTitle = responseStarted
-            ? "Default Not Found Page"
-            : "Re-executed page";
-        Browser.Equal(expectedTitle, () => Browser.Title);
-    }
-}
diff --git a/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs b/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs
index 0595baa95bc0..7a1163de31c8 100644
--- a/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs
+++ b/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs
@@ -37,11 +37,7 @@ public void CanRenderNotFoundInteractive(string renderingMode, bool useCustomNot
 
         if (useCustomNotFoundPage)
         {
-            var infoText = Browser.FindElement(By.Id("test-info")).Text;
-            Assert.Contains("Welcome On Custom Not Found Page", infoText);
-            // custom page should have a custom layout
-            var aboutLink = Browser.FindElement(By.Id("about-link")).Text;
-            Assert.Contains("About", aboutLink);
+            AssertNotFoundPageRendered();
         }
         else
         {
@@ -140,4 +136,68 @@ public void CanNavigateBetweenStaticPagesViaEnhancedNav()
         Browser.Equal("Global interactivity page: Static via attribute", () => h1.Text);
         Browser.Equal("static", () => Browser.Exists(By.Id("execution-mode")).Text);
     }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public void CanRenderNotFoundPage_SSR(bool streamingStarted)
+    {
+        string streamingPath = streamingStarted ? "-streaming" : "";
+        Navigate($"{ServerPathBase}/set-not-found-ssr{streamingPath}?useCustomNotFoundPage=true");
+        AssertNotFoundPageRendered();
+    }
+
+    [Theory]
+    [InlineData("ServerNonPrerendered")]
+    [InlineData("WebAssemblyNonPrerendered")]
+    public void CanRenderNotFoundPage_Interactive(string renderMode)
+    {
+        Navigate($"{ServerPathBase}/set-not-found?useCustomNotFoundPage=true&renderMode={renderMode}");
+        AssertNotFoundPageRendered();
+    }
+
+    private void AssertNotFoundPageRendered()
+    {
+        Browser.Equal("Welcome On Custom Not Found Page", () => Browser.FindElement(By.Id("test-info")).Text);
+        // custom page should have a custom layout
+        Browser.Equal("About", () => Browser.FindElement(By.Id("about-link")).Text);
+    }
+
+    [Theory]
+    [InlineData(false)]
+    [InlineData(true)]
+    public void CanRenderNotFoundIfNotFoundPageTypeNotProvided_SSR(bool streamingStarted)
+    {
+        string streamingPath = streamingStarted ? "-streaming" : "";
+        Navigate($"{ServerPathBase}/reexecution/set-not-found-ssr{streamingPath}");
+        if (streamingStarted)
+        {
+            AssertReExecutedPageRendered();
+        }
+        else
+        {
+            AssertNotFoundFragmentRendered();
+        }
+    }
+
+    [Theory]
+    [InlineData("ServerNonPrerendered")]
+    [InlineData("WebAssemblyNonPrerendered")]
+    public void DoesNotReExecuteIf404WasHandled_Interactive(string renderMode)
+    {
+        Navigate($"{ServerPathBase}/reexecution/set-not-found?renderMode={renderMode}");
+        AssertNotFoundFragmentRendered();
+    }
+
+    private void AssertNotFoundFragmentRendered() =>
+        Browser.Equal("There's nothing here", () => Browser.FindElement(By.Id("not-found-fragment")).Text);
+
+    [Fact]
+    public void StatusCodePagesWithReExecution()
+    {
+        Navigate($"{ServerPathBase}/reexecution/trigger-404");
+        AssertReExecutedPageRendered();
+    }
+    private void AssertReExecutedPageRendered() =>
+        Browser.Equal("Welcome On Page Re-executed After Not Found Event", () => Browser.Exists(By.Id("test-info")).Text);
 }
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs
index 4520d9202397..92fe46b53a3a 100644
--- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs
@@ -50,6 +50,15 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
         {
             app.Map("/reexecution", reexecutionApp =>
             {
+                app.Map("/trigger-404", trigger404App =>
+                {
+                    trigger404App.Run(async context =>
+                    {
+                        context.Response.StatusCode = 404;
+                        await context.Response.WriteAsync("Triggered a 404 status code.");
+                    });
+                });
+
                 if (!env.IsDevelopment())
                 {
                     app.UseExceptionHandler("/Error", createScopeForErrors: true);
@@ -62,7 +71,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
                 reexecutionApp.UseAntiforgery();
                 reexecutionApp.UseEndpoints(endpoints =>
                 {
-                    endpoints.MapRazorComponents<TRootComponent>();
+                    endpoints.MapRazorComponents<TRootComponent>()
+                        .AddAdditionalAssemblies(Assembly.Load("TestContentPackage"));
                 });
             });
 
@@ -83,7 +93,8 @@ private void ConfigureSubdirPipeline(IApplicationBuilder app, IWebHostEnvironmen
         app.UseAntiforgery();
         app.UseEndpoints(endpoints =>
         {
-            endpoints.MapRazorComponents<TRootComponent>();
+            endpoints.MapRazorComponents<TRootComponent>()
+                .AddAdditionalAssemblies(Assembly.Load("TestContentPackage"));
         });
     }
 }
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs
index 4bb4bee5014c..dacdc2b72b5a 100644
--- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs
@@ -88,6 +88,14 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
         {
             app.Map("/reexecution", reexecutionApp =>
             {
+                app.Map("/trigger-404", app =>
+                {
+                    app.Run(async context =>
+                    {
+                        context.Response.StatusCode = 404;
+                        await context.Response.WriteAsync("Triggered a 404 status code.");
+                    });
+                });
                 reexecutionApp.UseStatusCodePagesWithReExecute("/not-found-reexecute", createScopeForErrors: true);
                 reexecutionApp.UseRouting();
 
@@ -138,6 +146,7 @@ private void ConfigureEndpoints(IApplicationBuilder app, IWebHostEnvironment env
             }
 
             _ = endpoints.MapRazorComponents<TRootComponent>()
+                .AddAdditionalAssemblies(Assembly.Load("TestContentPackage"))
                 .AddAdditionalAssemblies(Assembly.Load("Components.WasmMinimal"))
                 .AddInteractiveServerRenderMode(options =>
                 {
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor
index ace8d627d941..2219da3955d2 100644
--- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor
@@ -1,5 +1,6 @@
 @using Components.TestServer.RazorComponents.Pages.Forms
 @using Components.WasmMinimal.Pages.NotFound
+@using TestContentPackage.NotFound
 
 @code {
     [Parameter]
@@ -10,7 +11,7 @@
 
     protected override void OnParametersSet()
     {
-        if (UseCustomNotFoundPage == "true")
+        if (string.Equals(UseCustomNotFoundPage, "true", StringComparison.OrdinalIgnoreCase))
         {
             NotFoundPageType = typeof(CustomNotFoundPage);
         }
@@ -29,12 +30,12 @@
     <HeadOutlet />
 </head>
 <body>
-    <Router AppAssembly="@typeof(App).Assembly" NotFoundPage="NotFoundPageType">
+    <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>There's nothing here</NotFound>
+        <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
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFound/PageThatSetsNotFound.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFound/PageThatSetsNotFound.razor
deleted file mode 100644
index e397f81672a3..000000000000
--- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFound/PageThatSetsNotFound.razor
+++ /dev/null
@@ -1,23 +0,0 @@
-@page "/reexecution/set-not-found"
-@page "/set-not-found"
-@attribute [StreamRendering(false)]
-@inject NavigationManager NavigationManager
-
-<PageTitle>Original page</PageTitle>
-
-<p id="test-info">Any content</p>
-
-@code{
-    [Parameter]
-    [SupplyParameterFromQuery(Name = "shouldSet")]
-    public bool? ShouldSet { get; set; }
-
-    protected override void OnInitialized()
-    {
-        bool shouldSet = ShouldSet ?? true;
-        if (shouldSet)
-        {
-            NavigationManager.NotFound();
-        }
-    }
-}
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/LargeStreamRendering.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/LargeStreamRendering.razor
index 9b6d199c76ee..99ea274e45d2 100644
--- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/LargeStreamRendering.razor
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/LargeStreamRendering.razor
@@ -2,6 +2,8 @@
 
 @attribute [StreamRendering]
 
+<PageTitle>Streaming</PageTitle>
+
 <h1>Streaming Rendering</h1>
 
 <NotEnabledStreamingRenderingComponent UseLargeItems="true"/>
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingRendering.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingRendering.razor
index 0bd36fd4817c..64f85052ae2d 100644
--- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingRendering.razor
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingRendering.razor
@@ -2,6 +2,8 @@
 
 @attribute [StreamRendering]
 
+<PageTitle>Streaming</PageTitle>
+
 <h1>Streaming Rendering</h1>
 
 <NotEnabledStreamingRenderingComponent />
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor
deleted file mode 100644
index 92b5f95d7c4b..000000000000
--- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor
+++ /dev/null
@@ -1,30 +0,0 @@
-@page "/reexecution/streaming-set-not-found"
-@page "/streaming-set-not-found"
-@attribute [StreamRendering]
-@inject NavigationManager NavigationManager
-
-@code {
-    [Parameter]
-    [SupplyParameterFromQuery(Name = "shouldSet")]
-    public bool? ShouldSet { get; set; }
-
-    [Parameter]
-    [SupplyParameterFromQuery(Name = "responseStarted")]
-    public bool? ResponseStarted { get; set; }
-
-    protected override async Task OnInitializedAsync()
-    {
-        bool shouldSet = ShouldSet ?? true;
-        bool responseStarted = ResponseStarted ?? true;
-        if (responseStarted)
-        {
-            // Simulate some delay before triggering NotFound to start streaming response
-            await Task.Yield();
-        }
-
-        if (shouldSet)
-        {
-            NavigationManager.NotFound();
-        }
-    }
-}
diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/NotFound/PageThatSetsNotFound-Interactive.razor b/src/Components/test/testassets/Components.WasmMinimal/Pages/NotFound/PageThatSetsNotFound-Interactive.razor
new file mode 100644
index 000000000000..fe0362ccda63
--- /dev/null
+++ b/src/Components/test/testassets/Components.WasmMinimal/Pages/NotFound/PageThatSetsNotFound-Interactive.razor
@@ -0,0 +1,28 @@
+@page "/reexecution/set-not-found"
+@page "/set-not-found"
+
+@*
+    this page is used only in global interactivity scenarios
+    the component's content will be rendered when it becomes interactive
+*@
+
+<TestContentPackage.NotFound.ComponentThatSetsNotFound @rendermode="@RenderModeHelper.GetRenderMode(_renderMode)" WaitForInteractivity="true" />
+
+@code{
+    [Parameter, SupplyParameterFromQuery(Name = "renderMode")]
+    public string? RenderModeStr { get; set; }
+
+    private RenderModeId _renderMode;
+
+    protected override void OnInitialized()
+    {
+        if (!string.IsNullOrEmpty(RenderModeStr))
+        {
+            _renderMode = RenderModeHelper.ParseRenderMode(RenderModeStr);
+        }
+        else
+        {
+            throw new ArgumentException("RenderModeStr cannot be null or empty. Did you mean to redirect to /set-not-found-ssr?", nameof(RenderModeStr));
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Components/test/testassets/Components.WasmMinimal/Routes.razor b/src/Components/test/testassets/Components.WasmMinimal/Routes.razor
index 92f41cc8b4f1..010176d437e7 100644
--- a/src/Components/test/testassets/Components.WasmMinimal/Routes.razor
+++ b/src/Components/test/testassets/Components.WasmMinimal/Routes.razor
@@ -1,5 +1,6 @@
 @using Microsoft.AspNetCore.Components.Routing
 @using Components.WasmMinimal.Pages.NotFound
+@using TestContentPackage.NotFound
 @inject NavigationManager NavigationManager
 
 @code {
@@ -11,7 +12,7 @@
 
     protected override void OnParametersSet()
     {
-        if (UseCustomNotFoundPage == "true")
+        if (string.Equals(UseCustomNotFoundPage, "true", StringComparison.OrdinalIgnoreCase))
         {
             NotFoundPageType = typeof(CustomNotFoundPage);
         }
@@ -22,10 +23,10 @@
     }
 }
 
-<Router AppAssembly="@typeof(Program).Assembly" NotFoundPage="NotFoundPageType">
+<Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(TestContentPackage.NotFound.NotFoundPage).Assembly }" NotFoundPage="NotFoundPageType">
     <Found Context="routeData">
         <RouteView RouteData="@routeData" />
         <FocusOnNavigate RouteData="@routeData" Selector="h1" />
     </Found>
-    <NotFound>There's nothing here</NotFound>
+    <NotFound><p id="not-found-fragment">There's nothing here</p></NotFound>
 </Router>
diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatPostsNotFound.razor b/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatPostsNotFound.razor
new file mode 100644
index 000000000000..f9b6cd9c7a3c
--- /dev/null
+++ b/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatPostsNotFound.razor
@@ -0,0 +1,32 @@
+@using Microsoft.AspNetCore.Components.Forms
+
+@inject NavigationManager NavigationManager
+
+@if (!WaitForInteractivity || RendererInfo.IsInteractive)
+{
+    <PageTitle>Original page</PageTitle>
+
+    <form method="post" id="not-found-form" @onsubmit="HandleSubmit" @formname="PostNotFoundForm">
+        <AntiforgeryToken />
+        <button type="submit">Trigger NotFound</button>
+    </form>
+
+    <p id="test-info">Any content</p>
+}
+
+@code{
+    [Parameter]
+    public bool StartStreaming { get; set; } = false;
+
+    [Parameter]
+    public bool WaitForInteractivity { get; set; } = false;
+
+    private async Task HandleSubmit()
+    {
+        if (StartStreaming)
+        {
+            await Task.Yield();
+        }
+        NavigationManager.NotFound();
+    }
+}
\ No newline at end of file
diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatSetsNotFound.razor b/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatSetsNotFound.razor
new file mode 100644
index 000000000000..fe024d3bb8d0
--- /dev/null
+++ b/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatSetsNotFound.razor
@@ -0,0 +1,26 @@
+@inject NavigationManager NavigationManager
+
+@if (!WaitForInteractivity || RendererInfo.IsInteractive)
+{
+    <PageTitle>Original page</PageTitle>
+
+    <p id="test-info">Any content</p>
+
+}
+
+@code{
+    [Parameter]
+    public bool StartStreaming { get; set; } = false;
+
+    [Parameter]
+    public bool WaitForInteractivity { get; set; } = false;
+
+    protected async override Task OnInitializedAsync()
+    {
+        if (StartStreaming)
+        {
+            await Task.Yield();
+        }
+        NavigationManager.NotFound();
+    }
+}
\ No newline at end of file
diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/NotFound/CustomNotFoundPage.razor b/src/Components/test/testassets/TestContentPackage/NotFound/CustomNotFoundPage.razor
similarity index 100%
rename from src/Components/test/testassets/Components.WasmMinimal/Pages/NotFound/CustomNotFoundPage.razor
rename to src/Components/test/testassets/TestContentPackage/NotFound/CustomNotFoundPage.razor
diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/Index.razor b/src/Components/test/testassets/TestContentPackage/NotFound/Index.razor
new file mode 100644
index 000000000000..5d0bd8b17ddc
--- /dev/null
+++ b/src/Components/test/testassets/TestContentPackage/NotFound/Index.razor
@@ -0,0 +1,89 @@
+@page "/not-found-index"
+
+@using Microsoft.AspNetCore.Components.Forms
+
+@inject NavigationManager NavigationManager
+
+<h1 id="test-info">List of Not Found test pages</h1>
+
+<h2>Link navigation (enhanced nav passed in Headers)</h2>
+<ul>
+    <li><a href="@PostNotFoundNoStreaming">@PostNotFoundNoStreamingText</a></li>
+    <li><a href="@PostNotFoundNoStreamingReexecution">@PostNotFoundNoStreamingReexecutionText</a></li>
+
+    <li><a href="@PostNotFoundStreaming">@PostNotFoundStreamingText</a></li>
+    <li><a href="@PostNotFoundStreamingReexecution">@PostNotFoundStreamingReexecutionText</a></li>
+
+    <li><a href="@SetNotFoundNoStreaming">@SetNotFoundNoStreamingText</a></li>
+    <li><a href="@SetNotFoundNoStreamingReexecution">@SetNotFoundNoStreamingReexecutionText</a></li>
+
+    <li><a href="@SetNotFoundStreaming">@SetNotFoundStreamingText</a></li>
+    <li><a href="@SetNotFoundStreamingReexecution">@SetNotFoundStreamingReexecutionText</a></li>
+</ul>
+
+<h2>Programmatic navigation in a form (enhanced nav not passed in Headers)</h2>
+<form method="post" @onsubmit="() => NavigateTo(PostNotFoundNoStreaming)" @formname="ProgrammaticPostNotFoundNoStreamingForm">
+    <AntiforgeryToken />
+    <button type="submit">Programmatic navigate to @PostNotFoundNoStreamingText</button>
+</form>
+<form method="post" @onsubmit="() => NavigateTo(PostNotFoundNoStreamingReexecution)" @formname="ProgrammaticPostNotFoundNoStreamingReexecutionForm">
+    <AntiforgeryToken />
+    <button type="submit">Programmatic navigate to @PostNotFoundNoStreamingReexecutionText</button>
+</form>
+
+<form method="post" @onsubmit="() => NavigateTo(PostNotFoundStreaming)" @formname="ProgrammaticPostNotFoundStreamingForm">
+    <AntiforgeryToken />
+    <button type="submit">Programmatic navigate to @PostNotFoundStreamingText</button>
+</form>
+<form method="post" @onsubmit="() => NavigateTo(PostNotFoundStreamingReexecution)" @formname="ProgrammaticPostNotFoundStreamingReexecutionForm">
+    <AntiforgeryToken />
+    <button type="submit">Programmatic navigate to @PostNotFoundStreamingReexecutionText</button>
+</form>
+
+<form method="post" @onsubmit="() => NavigateTo(SetNotFoundNoStreaming)" @formname="ProgrammaticSetNotFoundNoStreamingForm">
+    <AntiforgeryToken />
+    <button type="submit">Programmatic navigate to @SetNotFoundNoStreamingText</button>
+</form>
+<form method="post" @onsubmit="() => NavigateTo(SetNotFoundNoStreamingReexecution)" @formname="ProgrammaticSetNotFoundNoStreamingReexecutionForm">
+    <AntiforgeryToken />
+    <button type="submit">Programmatic navigate to @SetNotFoundNoStreamingReexecutionText</button>
+</form>
+
+<form method="post" @onsubmit="() => NavigateTo(SetNotFoundStreaming)" @formname="ProgrammaticSetNotFoundStreamingForm">
+    <AntiforgeryToken />
+    <button type="submit">Programmatic navigate to @SetNotFoundStreamingText</button>
+</form>
+<form method="post" @onsubmit="() => NavigateTo(SetNotFoundStreamingReexecution)" @formname="ProgrammaticSetNotFoundStreamingReexecutionForm">
+    <AntiforgeryToken />
+    <button type="submit">Programmatic navigate to @SetNotFoundStreamingReexecutionText</button>
+</form>
+
+@code {
+    [SupplyParameterFromQuery]
+    public bool useCustomNotFoundPage { get; set; }
+
+    private string AppendQuery(string url) => $"{url}?useCustomNotFoundPage={useCustomNotFoundPage}";
+
+    private string PostNotFoundNoStreaming => AppendQuery("post-not-found-ssr");
+    private string PostNotFoundNoStreamingReexecution => AppendQuery($"reexecution/post-not-found-ssr");
+    private string PostNotFoundStreaming => AppendQuery("post-not-found-ssr-streaming");
+    private string PostNotFoundStreamingReexecution => AppendQuery($"reexecution/post-not-found-ssr-streaming");
+    private string SetNotFoundNoStreaming => AppendQuery("set-not-found-ssr");
+    private string SetNotFoundNoStreamingReexecution => AppendQuery($"reexecution/set-not-found-ssr");
+    private string SetNotFoundStreaming => AppendQuery("set-not-found-ssr-streaming");
+    private string SetNotFoundStreamingReexecution => AppendQuery($"reexecution/set-not-found-ssr-streaming");
+
+    private const string PostNotFoundNoStreamingText = "PageThatPostsNotFound-no-streaming";
+    private const string PostNotFoundNoStreamingReexecutionText = $"{PostNotFoundNoStreamingText} with reexecution";
+    private const string PostNotFoundStreamingText = "PageThatPostsNotFound-streaming";
+    private const string PostNotFoundStreamingReexecutionText = $"{PostNotFoundStreamingText} with reexecution";
+    private const string SetNotFoundNoStreamingText = "PageThatSetsNotFound-no-streaming";
+    private const string SetNotFoundNoStreamingReexecutionText = $"{SetNotFoundNoStreamingText} with reexecution";
+    private const string SetNotFoundStreamingText = "PageThatSetsNotFound-streaming";
+    private const string SetNotFoundStreamingReexecutionText = $"{SetNotFoundStreamingText} with reexecution";
+
+    private void NavigateTo(string url)
+    {
+        NavigationManager.NavigateTo(url);
+    }
+}
\ No newline at end of file
diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/NotFound/NotFoundLayout.razor b/src/Components/test/testassets/TestContentPackage/NotFound/NotFoundLayout.razor
similarity index 100%
rename from src/Components/test/testassets/Components.WasmMinimal/Pages/NotFound/NotFoundLayout.razor
rename to src/Components/test/testassets/TestContentPackage/NotFound/NotFoundLayout.razor
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFound/NotFoundPage.razor b/src/Components/test/testassets/TestContentPackage/NotFound/NotFoundPage.razor
similarity index 100%
rename from src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFound/NotFoundPage.razor
rename to src/Components/test/testassets/TestContentPackage/NotFound/NotFoundPage.razor
diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-no-streaming.razor b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-no-streaming.razor
new file mode 100644
index 000000000000..4aa3a245b047
--- /dev/null
+++ b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-no-streaming.razor
@@ -0,0 +1,11 @@
+@page "/reexecution/post-not-found-ssr"
+@page "/post-not-found-ssr"
+@attribute [StreamRendering(false)]
+
+@*
+    this page is used in global interactivity and no interactivity scenarios
+    the content is rendered on the server without streaming and might become
+    interactive later if interactivity was enabled in the app
+*@
+
+<TestContentPackage.NotFound.ComponentThatPostsNotFound />
\ No newline at end of file
diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-streaming.razor b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-streaming.razor
new file mode 100644
index 000000000000..a542c35c52f4
--- /dev/null
+++ b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-streaming.razor
@@ -0,0 +1,11 @@
+@page "/reexecution/post-not-found-ssr-streaming"
+@page "/post-not-found-ssr-streaming"
+@attribute [StreamRendering(true)]
+
+@*
+    this page is used in global interactivity and no interactivity scenarios
+    the content is rendered on the server with streaming and might become
+    interactive later if interactivity was enabled in the app
+*@
+
+<TestContentPackage.NotFound.ComponentThatPostsNotFound StartStreaming="true"/>
\ No newline at end of file
diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-no-streaming.razor b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-no-streaming.razor
new file mode 100644
index 000000000000..b99ed19711d7
--- /dev/null
+++ b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-no-streaming.razor
@@ -0,0 +1,11 @@
+@page "/reexecution/set-not-found-ssr"
+@page "/set-not-found-ssr"
+@attribute [StreamRendering(false)]
+
+@*
+    this page is used in global interactivity and no interactivity scenarios
+    the content is rendered on the server without streaming and might become
+    interactive later if interactivity was enabled in the app
+*@
+
+<TestContentPackage.NotFound.ComponentThatSetsNotFound />
\ No newline at end of file
diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-streaming.razor b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-streaming.razor
new file mode 100644
index 000000000000..e3124758ce65
--- /dev/null
+++ b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-streaming.razor
@@ -0,0 +1,11 @@
+@page "/reexecution/set-not-found-ssr-streaming"
+@page "/set-not-found-ssr-streaming"
+@attribute [StreamRendering(true)]
+
+@*
+    this page is used in global interactivity and no interactivity scenarios
+    the content is rendered on the server with streaming and might become
+    interactive later if interactivity was enabled in the app
+*@
+
+<TestContentPackage.NotFound.ComponentThatSetsNotFound StartStreaming="true"/>
\ No newline at end of file
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFound/ReexecutedPage.razor b/src/Components/test/testassets/TestContentPackage/NotFound/ReexecutedPage.razor
similarity index 100%
rename from src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFound/ReexecutedPage.razor
rename to src/Components/test/testassets/TestContentPackage/NotFound/ReexecutedPage.razor
diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs
index 6aa6843995b8..952753c69830 100644
--- a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs
+++ b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs
@@ -187,9 +187,11 @@ public static IApplicationBuilder UseStatusCodePagesWithReExecute(
             {
                 var newNext = RerouteHelper.Reroute(app, routeBuilder, next);
                 return new StatusCodePagesMiddleware(next,
-                    Options.Create(new StatusCodePagesOptions() {
+                    Options.Create(new StatusCodePagesOptions()
+                    {
                         HandleAsync = CreateHandler(pathFormat, queryFormat, newNext),
-                        CreateScopeForErrors = createScopeForErrors
+                        CreateScopeForErrors = createScopeForErrors,
+                        PathFormat = pathFormat
                     })).Invoke;
             });
         }
@@ -197,7 +199,8 @@ public static IApplicationBuilder UseStatusCodePagesWithReExecute(
         var options = new StatusCodePagesOptions
         {
             HandleAsync = CreateHandler(pathFormat, queryFormat),
-            CreateScopeForErrors = createScopeForErrors
+            CreateScopeForErrors = createScopeForErrors,
+            PathFormat = pathFormat
         };
         var wrappedOptions = new OptionsWrapper<StatusCodePagesOptions>(options);
         return app.UseMiddleware<StatusCodePagesMiddleware>(wrappedOptions);
diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesMiddleware.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesMiddleware.cs
index 87994f6c7011..7b821aa01b2e 100644
--- a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesMiddleware.cs
+++ b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesMiddleware.cs
@@ -48,8 +48,14 @@ public async Task Invoke(HttpContext context)
             statusCodeFeature.Enabled = false;
         }
 
+        // Attach pathFormat to HttpContext.Items early in the pipeline
+        context.Items[nameof(StatusCodePagesOptions)] = _options.PathFormat;
+
         await _next(context);
 
+        // Remove pathFormat from HttpContext.Items after handler execution
+        context.Items.Remove(nameof(StatusCodePagesOptions));
+
         if (!statusCodeFeature.Enabled)
         {
             // Check if the feature is still available because other middleware (such as a web API written in MVC) could
diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs
index fba436b01442..95ff38eed8d5 100644
--- a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs
+++ b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs
@@ -62,4 +62,6 @@ private static string BuildResponseBody(int httpStatusCode)
     /// </summary>
     /// <remarks>The default value is <see langword="false"/>.</remarks>
     public bool CreateScopeForErrors { get; set; }
+
+    internal string? PathFormat { get; set; }
 }
diff --git a/src/Shared/E2ETesting/WaitAssert.cs b/src/Shared/E2ETesting/WaitAssert.cs
index 71c8ae6b7c41..a3bb53520f30 100644
--- a/src/Shared/E2ETesting/WaitAssert.cs
+++ b/src/Shared/E2ETesting/WaitAssert.cs
@@ -37,6 +37,11 @@ public static void True(this IWebDriver driver, Func<bool> actual, TimeSpan time
 
     public static void True(this IWebDriver driver, Func<bool> actual, TimeSpan timeout, string message)
         => WaitAssertCore(driver, () => Assert.True(actual(), message), timeout);
+    public static void True(this IWebDriver driver, Func<bool> actual, string message)
+        => WaitAssertCore(driver, () => Assert.True(actual(), message));
+
+    public static void False(this IWebDriver driver, Func<bool> actual, string message)
+        => WaitAssertCore(driver, () => Assert.False(actual(), message));
 
     public static void False(this IWebDriver driver, Func<bool> actual)
         => WaitAssertCore(driver, () => Assert.False(actual()));