diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index cdf17e376a00..1b1976463464 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -81,10 +81,7 @@ private Task ReturnErrorResponse(string detailedMessage) internal async Task SetNotFoundResponseAsync(string baseUri, NotFoundEventArgs args) { - 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 (_httpContext.Response.HasStarted) { if (string.IsNullOrEmpty(_notFoundUrl)) { @@ -104,7 +101,7 @@ internal async Task SetNotFoundResponseAsync(string baseUri, NotFoundEventArgs a // When the application triggers a NotFound event, we continue rendering the current batch. // However, after completing this batch, we do not want to process any further UI updates, // as we are going to return a 404 status and discard the UI updates generated so far. - SignalRendererToFinishRendering(); + RequestRendererToFinishRendering(); } private string GetNotFoundUrl(string baseUri, NotFoundEventArgs args) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index 16c44c92f641..39a107e2588f 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -149,6 +149,8 @@ internal async ValueTask RenderEndpointComponen var component = BeginRenderingComponent(rootComponentType, parameters); var result = new PrerenderedComponentHtmlContent(Dispatcher, component); + FinishRendereingOnQuiescenceIfRequested(result); + await WaitForResultReady(waitForQuiescence, result); return result; @@ -159,6 +161,19 @@ internal async ValueTask RenderEndpointComponen } } + private void FinishRendereingOnQuiescenceIfRequested(PrerenderedComponentHtmlContent htmlContent) + { + if (htmlContent.QuiescenceTask.IsCompleted) + { + SignalRendererToFinishRendering(); + return; + } + + htmlContent.QuiescenceTask.ContinueWith( + _ => SignalRendererToFinishRendering(), + TaskScheduler.Default); + } + private async Task WaitForResultReady(bool waitForQuiescence, PrerenderedComponentHtmlContent result) { if (waitForQuiescence) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index f5d0699e1efe..abcbbd47a138 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -44,6 +44,7 @@ internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrer private HttpContext _httpContext = default!; // Always set at the start of an inbound call private ResourceAssetCollection? _resourceCollection; private bool _rendererIsStopped; + private bool _rendererStopRequested; private readonly ILogger _logger; // The underlying Renderer always tracks the pending tasks representing *full* quiescence, i.e., @@ -185,11 +186,19 @@ protected override void AddPendingTask(ComponentState? componentState, Task task base.AddPendingTask(componentState, task); } + internal void RequestRendererToFinishRendering() + { + // requests a deferred stop of the renderer, which will have an effect after the current batch is completed + _rendererStopRequested = true; + } + // For testing purposes only internal void SignalRendererToFinishRendering() { - // sets a deferred stop on the renderer, which will have an effect after the current batch is completed - _rendererIsStopped = true; + if (_rendererStopRequested) + { + _rendererIsStopped = true; + } } protected override void ProcessPendingRender() diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index bbeb1ad4d9bb..698e26da88b4 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -141,14 +141,22 @@ public void NotFoundSetOnInitialization_ResponseNotStarted_SSR(bool hasReExecuti string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr?useCustomNotFoundPage={hasCustomNotFoundPageSet}"; Navigate(testUrl); - if (hasCustomNotFoundPageSet) - { - AssertNotFoundPageRendered(); - } - else - { - AssertNotFoundFragmentRendered(); - } + AssertNotFoundRendered_ResponseNotStarted(hasCustomNotFoundPageSet); + AssertUrlNotChanged(testUrl); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void NotFoundSetOnInitialization_AfterAsyncOperation_ResponseNotStarted_SSR(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet) + { + string reexecution = hasReExecutionMiddleware ? "/reexecution" : ""; + string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr?doAsync=true&useCustomNotFoundPage={hasCustomNotFoundPageSet}"; + Navigate(testUrl); + + AssertNotFoundRendered_ResponseNotStarted(hasCustomNotFoundPageSet); AssertUrlNotChanged(testUrl); } @@ -162,7 +170,8 @@ public void NotFoundSetOnInitialization_ResponseStarted_SSR(bool hasReExecutionM string reexecution = hasReExecutionMiddleware ? "/reexecution" : ""; string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr-streaming?useCustomNotFoundPage={hasCustomNotFoundPageSet}"; Navigate(testUrl); - AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); + + AssertNotFoundRendered_ResponseStarted(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); AssertUrlNotChanged(testUrl); } @@ -176,11 +185,12 @@ public void NotFoundSetOnInitialization_ResponseStarted_EnhancedNavigationDisabl string reexecution = hasReExecutionMiddleware ? "/reexecution" : ""; string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr-streaming?useCustomNotFoundPage={hasCustomNotFoundPageSet}"; Navigate(testUrl); - AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); + + AssertNotFoundRendered_ResponseStarted(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); AssertUrlChanged(testUrl); } - private void AssertNotFoundRendered_ResponseStarted_Or_POST(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet, string testUrl) + private void AssertNotFoundRendered_ResponseStarted(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet, string testUrl) { if (hasCustomNotFoundPageSet) { @@ -197,6 +207,18 @@ private void AssertNotFoundRendered_ResponseStarted_Or_POST(bool hasReExecutionM } } + private void AssertNotFoundRendered_ResponseNotStarted(bool hasCustomNotFoundPageSet) + { + if (hasCustomNotFoundPageSet) + { + AssertNotFoundPageRendered(); + } + else + { + AssertNotFoundFragmentRendered(); + } + } + [Theory] [InlineData(true, true)] [InlineData(true, false)] @@ -209,7 +231,23 @@ public void NotFoundSetOnFormSubmit_ResponseNotStarted_SSR(bool hasReExecutionMi Navigate(testUrl); Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click(); - AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); + AssertNotFoundRendered_ResponseStarted(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); + AssertUrlNotChanged(testUrl); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void NotFoundSetOnFormSubmit_AfterAsyncOperation_ResponseNotStarted_SSR(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet) + { + string reexecution = hasReExecutionMiddleware ? "/reexecution" : ""; + string testUrl = $"{ServerPathBase}{reexecution}/post-not-found-ssr?doAsync=true&useCustomNotFoundPage={hasCustomNotFoundPageSet}"; + Navigate(testUrl); + Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click(); + + AssertNotFoundRendered_ResponseNotStarted(hasCustomNotFoundPageSet); AssertUrlNotChanged(testUrl); } @@ -225,7 +263,7 @@ public void NotFoundSetOnFormSubmit_ResponseStarted_SSR(bool hasReExecutionMiddl Navigate(testUrl); Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click(); - AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); + AssertNotFoundRendered_ResponseStarted(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); AssertUrlNotChanged(testUrl); } diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatPostsNotFound.razor b/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatPostsNotFound.razor index f9b6cd9c7a3c..1a878d861de5 100644 --- a/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatPostsNotFound.razor +++ b/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatPostsNotFound.razor @@ -1,4 +1,4 @@ -@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Forms @inject NavigationManager NavigationManager @@ -16,17 +16,17 @@ @code{ [Parameter] - public bool StartStreaming { get; set; } = false; + public bool DoAsyncOperationBeforeSettingNotFound { get; set; } = false; [Parameter] public bool WaitForInteractivity { get; set; } = false; private async Task HandleSubmit() { - if (StartStreaming) + if (DoAsyncOperationBeforeSettingNotFound) { 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 index fe024d3bb8d0..26ffd71e3e64 100644 --- a/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatSetsNotFound.razor +++ b/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatSetsNotFound.razor @@ -1,4 +1,4 @@ -@inject NavigationManager NavigationManager +@inject NavigationManager NavigationManager @if (!WaitForInteractivity || RendererInfo.IsInteractive) { @@ -10,17 +10,17 @@ @code{ [Parameter] - public bool StartStreaming { get; set; } = false; + public bool DoAsyncOperationBeforeSettingNotFound { get; set; } = false; [Parameter] public bool WaitForInteractivity { get; set; } = false; protected async override Task OnInitializedAsync() { - if (StartStreaming) + if (DoAsyncOperationBeforeSettingNotFound) { await Task.Yield(); } NavigationManager.NotFound(); } -} \ No newline at end of file +} diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-no-streaming.razor b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-no-streaming.razor index 4aa3a245b047..0a48cfb42c29 100644 --- a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-no-streaming.razor +++ b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-no-streaming.razor @@ -8,4 +8,9 @@ interactive later if interactivity was enabled in the app *@ - \ No newline at end of file + + +@code{ + [SupplyParameterFromQuery(Name = "doAsync")] + public bool DoAsync { get; set; } = false; +} diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-streaming.razor b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-streaming.razor index a542c35c52f4..834c977d943b 100644 --- a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-streaming.razor +++ b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-streaming.razor @@ -8,4 +8,4 @@ interactive later if interactivity was enabled in the app *@ - \ No newline at end of file + \ 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 index b99ed19711d7..e72b821a2b18 100644 --- a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-no-streaming.razor +++ b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-no-streaming.razor @@ -8,4 +8,9 @@ interactive later if interactivity was enabled in the app *@ - \ No newline at end of file + + +@code{ + [SupplyParameterFromQuery(Name = "doAsync")] + public bool DoAsync { get; set; } = false; +} diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-streaming.razor b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-streaming.razor index e3124758ce65..7574e5920396 100644 --- a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-streaming.razor +++ b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-streaming.razor @@ -8,4 +8,4 @@ interactive later if interactivity was enabled in the app *@ - \ No newline at end of file + \ No newline at end of file