Design Proposal: Dynamic/Async paths for StatusCodePages and ExceptionHandler middleware
See original GitHub issueIssue
An issue exists requesting a solution to setting the re-execute path for the StatusCodePages and ExceptionHandler middleware. https://github.com/dotnet/aspnetcore/issues/18383
While both middleware allow for a custom handler/pipeline to be used, if the original functionality provided by ASPNET Core is the goal, but a “dynamic” path is needed, then developers will currently need to copy source from ASPNET Core to re-create the original functionality. If they want the ExceptionHandler middleware to be performant when acquiring a dynamic error path, they also need to re-create internal
framework code, chasing a breadcrumb trail (ex: DiagnosticsLoggerExtensions
and Resources.resx
).
Both the UseStatusCodePagesWithReExecute
and UseExceptionHandler
are robust and the most convenient ways to gracefully handle errors in the framework, so copying ASPNET Core source into multiple applications is the route I’ve taken and I’d like to avoid this, while also limiting the impact to the framework to have this ‘baked in’.
Value
Needing dynamic path values for these middleware is common in Content Management Systems (or any application with runtime dynamic routes) where URLs are generated/stored in the database and need to be discovered at runtime.
Another scenario where dynamic paths would be important is if the re-executed path had culture information in it - a subdomain or a path prefix (ex: /es-mx/not-found
) cannot be static as it’s dependent on the request/culture.
Proposed Solution
Both of these middleware are customizable through their options:
- UseStatusCodePagesWithReExecute with
pathFormat
- UseExceptionHandler with
ExceptionHandlerOptions
Since providing options through the extensions is the existing pattern, we could add a way to provide a Func<SomeContext, Task<string>>? PathRetriever
.
The delegate, if not null, would be executed to acquire the path string. However, we also don’t want to perform the potentially expensive operation of acquiring the path string until it is needed, so this value would need to be passed down to where the static path is used to setup the re-execution of the pipeline to handle the errors.
UseStatusCodePagesWithReExecute
//// New overload of extension method ////
public static IApplicationBuilder UseStatusCodePagesWithReExecute(
this IApplicationBuilder app,
Func<StatusCodeContext, Task<string>> pathRetriever)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
const string globalRouteBuilderKey = "__GlobalEndpointRouteBuilder";
// Only use this path if there's a global router (in the 'WebApplication' case).
if (app.Properties.TryGetValue(globalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null)
{
return app.Use(next =>
{
/** Same as existing **/
return new StatusCodePagesMiddleware(next,
//// Pass the new delegate to the CreateHandler method ////
Options.Create(new StatusCodePagesOptions() { HandleAsync = CreateHandler(null, null, pathRetriever, newNext) })).Invoke;
});
}
//// Pass the new delegate to the CreateHandler method ////
return app.UseStatusCodePages(CreateHandler(null, null, pathRetriever));
}
private static Func<StatusCodeContext, Task> CreateHandler(string? pathFormat, string? queryFormat, Func<StatusCodeContext, Task<string>>? pathRetriever, RequestDelegate? next = null)
{
var handler = async (StatusCodeContext context) =>
{
//// New logic to retrieve path dynamically ////
pathFormat = pathRetriever is not null
? await pathRetriever(context)
: pathFormat;
// Same as existing
};
return handler;
}
UseExceptionHandler
public class ExceptionHandlerOptions
{
public PathString ExceptionHandlingPath { get; set; }
public RequestDelegate? ExceptionHandler { get; set; }
public bool AllowStatusCode404Response { get; set; }
//// New optional property ////
public Func<HttpContext, Task<string>>? PathRetriever { get; set; }
}
public class ExceptionHandlerMiddleware
{
/** Same as existing **/
private async Task HandleException(HttpContext context, ExceptionDispatchInfo edi)
{
_logger.UnhandledException(edi.SourceException);
// We can't do anything if the response has already started, just abort.
if (context.Response.HasStarted)
{
_logger.ResponseStartedErrorHandler();
edi.Throw();
}
PathString originalPath = context.Request.Path;
//// New logic to retrieve path dynamically ////
if (_options.PathRetriever is not null)
{
context.Request.Path = await _options.PathRetriever(context);
}
else if (_options.ExceptionHandlingPath.HasValue)
{
context.Request.Path = _options.ExceptionHandlingPath;
}
/** Same as existing **/
}
}
These additions would enable developers to specify the re-execute path dynamically, using the context of the error, incurring the cost of the async
delegate only when the errors occur.
I’d be happy to follow up this proposal with a PR if it seems there is enough value to these changes. If not, I’ll continue copying source code and manually keeping it up to date as I upgrade TFMs in multiple projects over time.
Thanks!
Issue Analytics
- State:
- Created 2 years ago
- Reactions:5
- Comments:10 (9 by maintainers)
Top GitHub Comments
A few more thoughts about the design first: Instead of
Func<HttpContext, Task<string>>
returning the new path, how aboutFunc<HttpContext, Task>
where you are responsible for updating the HttpContext directly? I’d also consider something likeFunc<HttpContext, Task<bool>>
where the return value could indicate if you even want to re-execute the request, or if you were able to handle the error inline.Can you try those variants and see how they work?
@MadL1me Go for it! Ping me on a PR when you create one.