Skip to content

Commit 005f217

Browse files
committed
Fix reexecution mechanism.
1 parent d41bd5b commit 005f217

File tree

8 files changed

+141
-3
lines changed

8 files changed

+141
-3
lines changed

src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ private async Task RenderComponentCore(HttpContext context)
4545
Log.InteractivityDisabledForErrorHandling(_logger);
4646
}
4747
_renderer.InitializeStreamingRenderingFraming(context, isErrorHandler, hasStatusCodePage);
48-
EndpointHtmlRenderer.MarkAsAllowingEnhancedNavigation(context);
48+
bool avoidEditingHeaders = hasStatusCodePage && context.Response.StatusCode == StatusCodes.Status404NotFound;
49+
if (!avoidEditingHeaders)
50+
{
51+
EndpointHtmlRenderer.MarkAsAllowingEnhancedNavigation(context);
52+
}
4953

5054
var endpoint = context.GetEndpoint() ?? throw new InvalidOperationException($"An endpoint must be set on the '{nameof(HttpContext)}'.");
5155

@@ -86,6 +90,8 @@ await _renderer.InitializeStandardComponentServicesAsync(
8690
await using var writer = new HttpResponseStreamWriter(context.Response.Body, Encoding.UTF8, defaultBufferSize, ArrayPool<byte>.Shared, ArrayPool<char>.Shared);
8791
using var bufferWriter = new BufferedTextWriter(writer);
8892

93+
int originalStatusCode = context.Response.StatusCode;
94+
8995
// Note that we always use Static rendering mode for the top-level output from a RazorComponentResult,
9096
// because you never want to serialize the invocation of RazorComponentResultHost. Instead, that host
9197
// component takes care of switching into your desired render mode when it produces its own output.
@@ -95,6 +101,16 @@ await _renderer.InitializeStandardComponentServicesAsync(
95101
ParameterView.Empty,
96102
waitForQuiescence: result.IsPost || isErrorHandler || hasStatusCodePage);
97103

104+
bool requresReexecution = originalStatusCode != context.Response.StatusCode && hasStatusCodePage;
105+
if (requresReexecution)
106+
{
107+
// If the response is a 404, we don't want to write any content.
108+
// This is because the 404 status code is used by the routing middleware
109+
// to indicate that no endpoint was found for the request.
110+
await bufferWriter.FlushAsync();
111+
return;
112+
}
113+
98114
Task quiesceTask;
99115
if (!result.IsPost)
100116
{

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ private void SetNotFoundResponse(object? sender, EventArgs args)
8484
return;
8585
}
8686
_httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
87+
_httpContext.Response.ContentType = null;
8788
SignalRendererToFinishRendering();
8889
}
8990

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
using Microsoft.AspNetCore.Components.Web.HtmlRendering;
99
using Microsoft.AspNetCore.Html;
1010
using Microsoft.AspNetCore.Http;
11+
using Microsoft.Extensions.Logging;
1112
using static Microsoft.AspNetCore.Internal.LinkerFlags;
1213

1314
namespace Microsoft.AspNetCore.Components.Endpoints;
1415

1516
internal partial class EndpointHtmlRenderer
1617
{
1718
private static readonly object ComponentSequenceKey = new object();
19+
private bool stopAddingTasks;
1820

1921
protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode)
2022
{
@@ -146,6 +148,7 @@ internal async ValueTask<PrerenderedComponentHtmlContent> RenderEndpointComponen
146148
{
147149
var component = BeginRenderingComponent(rootComponentType, parameters);
148150
var result = new PrerenderedComponentHtmlContent(Dispatcher, component);
151+
stopAddingTasks = httpContext.Response.StatusCode == StatusCodes.Status404NotFound && waitForQuiescence;
149152

150153
await WaitForResultReady(waitForQuiescence, result);
151154

@@ -166,7 +169,52 @@ private async Task WaitForResultReady(bool waitForQuiescence, PrerenderedCompone
166169
}
167170
else if (_nonStreamingPendingTasks.Count > 0)
168171
{
169-
await WaitForNonStreamingPendingTasks();
172+
if (stopAddingTasks)
173+
{
174+
HandleNonStreamingTasks();
175+
}
176+
else
177+
{
178+
await WaitForNonStreamingPendingTasks();
179+
}
180+
}
181+
}
182+
183+
public void HandleNonStreamingTasks()
184+
{
185+
if (NonStreamingPendingTasksCompletion == null)
186+
{
187+
// Iterate over the tasks and handle their exceptions
188+
foreach (var task in _nonStreamingPendingTasks)
189+
{
190+
_ = GetErrorHandledTask(task); // Fire-and-forget with exception handling
191+
}
192+
193+
// Clear the pending tasks since we are handling them
194+
_nonStreamingPendingTasks.Clear();
195+
196+
// Mark the tasks as completed
197+
NonStreamingPendingTasksCompletion = Task.CompletedTask;
198+
}
199+
}
200+
201+
private async Task GetErrorHandledTask(Task taskToHandle)
202+
{
203+
try
204+
{
205+
await taskToHandle;
206+
}
207+
catch (Exception ex)
208+
{
209+
// Ignore errors due to task cancellations.
210+
if (!taskToHandle.IsCanceled)
211+
{
212+
_logger.LogError(
213+
ex,
214+
@"An exception occurred during non-streaming rendering.
215+
This exception will be ignored because the response
216+
is being discarded and the request is being re-executed.");
217+
}
170218
}
171219
}
172220

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public void InitializeStreamingRenderingFraming(HttpContext httpContext, bool is
2828
{
2929
_isHandlingErrors = isErrorHandler;
3030
_hasStatusCodePage = hasStatusCodePage;
31-
if (IsProgressivelyEnhancedNavigation(httpContext.Request))
31+
bool avoidEditingHeaders = hasStatusCodePage && httpContext.Response.StatusCode == StatusCodes.Status404NotFound;
32+
if (!avoidEditingHeaders && IsProgressivelyEnhancedNavigation(httpContext.Request))
3233
{
3334
var id = Guid.NewGuid().ToString();
3435
httpContext.Response.Headers.Add(_streamingRenderingFramingHeaderName, id);

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrer
4444
private HttpContext _httpContext = default!; // Always set at the start of an inbound call
4545
private ResourceAssetCollection? _resourceCollection;
4646
private bool _rendererIsStopped;
47+
private readonly ILogger _logger;
4748

4849
// The underlying Renderer always tracks the pending tasks representing *full* quiescence, i.e.,
4950
// when everything (regardless of streaming SSR) is fully complete. In this subclass we also track
@@ -56,6 +57,7 @@ public EndpointHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory log
5657
{
5758
_services = serviceProvider;
5859
_options = serviceProvider.GetRequiredService<IOptions<RazorComponentsServiceOptions>>().Value;
60+
_logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Components.RenderTree.Renderer");
5961
}
6062

6163
internal HttpContext? HttpContext => _httpContext;
@@ -163,6 +165,11 @@ protected override ComponentState CreateComponentState(int componentId, ICompone
163165

164166
protected override void AddPendingTask(ComponentState? componentState, Task task)
165167
{
168+
if (stopAddingTasks)
169+
{
170+
return;
171+
}
172+
166173
var streamRendering = componentState is null
167174
? false
168175
: ((EndpointComponentState)componentState).StreamRendering;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Builder.StatusCodePagesOptions.CreateScopeForErrors.get -> bool
3+
Microsoft.AspNetCore.Builder.StatusCodePagesOptions.CreateScopeForErrors.set -> void
4+
static Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithReExecute(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, string! pathFormat, string? queryFormat = null, bool createScopeForErrors = false) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!

src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
45
using System.Globalization;
56
using Microsoft.AspNetCore.Diagnostics;
67
using Microsoft.AspNetCore.Http;
78
using Microsoft.AspNetCore.Http.Features;
89
using Microsoft.AspNetCore.Routing;
10+
using Microsoft.Extensions.DependencyInjection;
911
using Microsoft.Extensions.Options;
1012

1113
namespace Microsoft.AspNetCore.Builder;
@@ -160,10 +162,50 @@ public static IApplicationBuilder UseStatusCodePagesWithReExecute(
160162
return app.UseStatusCodePages(CreateHandler(pathFormat, queryFormat));
161163
}
162164

165+
/// <summary>
166+
/// Adds a StatusCodePages middleware to the pipeline. Specifies that the response body should be generated by
167+
/// re-executing the request pipeline using an alternate path. This path may contain a '{0}' placeholder of the status code.
168+
/// </summary>
169+
/// <param name="app"></param>
170+
/// <param name="pathFormat"></param>
171+
/// <param name="queryFormat"></param>
172+
/// <param name="createScopeForErrors">Whether or not to create a new <see cref="IServiceProvider"/> scope.</param>
173+
/// <returns></returns>
174+
[SuppressMessage("ApiDesign", "RS0026:Do not add multiple overloads with optional parameters", Justification = "Required to maintain compatibility")]
175+
public static IApplicationBuilder UseStatusCodePagesWithReExecute(
176+
this IApplicationBuilder app,
177+
string pathFormat,
178+
string? queryFormat = null,
179+
bool createScopeForErrors = false)
180+
{
181+
ArgumentNullException.ThrowIfNull(app);
182+
183+
// Only use this path if there's a global router (in the 'WebApplication' case).
184+
if (app.Properties.TryGetValue(RerouteHelper.GlobalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null)
185+
{
186+
return app.Use(next =>
187+
{
188+
var newNext = RerouteHelper.Reroute(app, routeBuilder, next);
189+
return new StatusCodePagesMiddleware(next,
190+
Options.Create(new StatusCodePagesOptions() {
191+
HandleAsync = CreateHandler(pathFormat, queryFormat, newNext),
192+
CreateScopeForErrors = createScopeForErrors
193+
})).Invoke;
194+
});
195+
}
196+
197+
return app.UseStatusCodePages(new StatusCodePagesOptions
198+
{
199+
HandleAsync = CreateHandler(pathFormat, queryFormat),
200+
CreateScopeForErrors = createScopeForErrors
201+
});
202+
}
203+
163204
private static Func<StatusCodeContext, Task> CreateHandler(string pathFormat, string? queryFormat, RequestDelegate? next = null)
164205
{
165206
var handler = async (StatusCodeContext context) =>
166207
{
208+
// context.Options.CreateScopeForErrors
167209
var originalStatusCode = context.HttpContext.Response.StatusCode;
168210

169211
var newPath = new PathString(
@@ -176,6 +218,10 @@ private static Func<StatusCodeContext, Task> CreateHandler(string pathFormat, st
176218
var originalQueryString = context.HttpContext.Request.QueryString;
177219

178220
var routeValuesFeature = context.HttpContext.Features.Get<IRouteValuesFeature>();
221+
var oldScope = context.Options.CreateScopeForErrors ? context.HttpContext.RequestServices : null;
222+
await using AsyncServiceScope? scope = context.Options.CreateScopeForErrors
223+
? context.HttpContext.RequestServices.GetRequiredService<IServiceScopeFactory>().CreateAsyncScope() // or .GetRequiredService<IServiceScopeFactory>().CreateAsyncScope()
224+
: null;
179225

180226
// Store the original paths so the app can check it.
181227
context.HttpContext.Features.Set<IStatusCodeReExecuteFeature>(new StatusCodeReExecuteFeature()
@@ -188,6 +234,11 @@ private static Func<StatusCodeContext, Task> CreateHandler(string pathFormat, st
188234
RouteValues = routeValuesFeature?.RouteValues
189235
});
190236

237+
if (scope.HasValue)
238+
{
239+
context.HttpContext.RequestServices = scope.Value.ServiceProvider;
240+
}
241+
191242
// An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset
192243
// the endpoint and route values to ensure things are re-calculated.
193244
HttpExtensions.ClearEndpoint(context.HttpContext);
@@ -210,6 +261,10 @@ private static Func<StatusCodeContext, Task> CreateHandler(string pathFormat, st
210261
context.HttpContext.Request.QueryString = originalQueryString;
211262
context.HttpContext.Request.Path = originalPath;
212263
context.HttpContext.Features.Set<IStatusCodeReExecuteFeature?>(null);
264+
if (oldScope != null)
265+
{
266+
context.HttpContext.RequestServices = oldScope;
267+
}
213268
}
214269
};
215270

src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,11 @@ private static string BuildResponseBody(int httpStatusCode)
5555
/// The handler that generates the response body for the given <see cref="StatusCodeContext"/>. By default this produces a plain text response that includes the status code.
5656
/// </summary>
5757
public Func<StatusCodeContext, Task> HandleAsync { get; set; }
58+
59+
/// <summary>
60+
/// Gets or sets whether the handler needs to create a separate <see cref="IServiceProvider"/> scope and
61+
/// replace it on <see cref="HttpContext.RequestServices"/> when re-executing the request.
62+
/// </summary>
63+
/// <remarks>The default value is <see langword="false"/>.</remarks>
64+
public bool CreateScopeForErrors { get; set; }
5865
}

0 commit comments

Comments
 (0)