Skip to content

Design Proposal: Dynamic/Async paths for StatusCodePages and ExceptionHandler middleware #40677

@seangwright

Description

@seangwright

Issue

An issue exists requesting a solution to setting the re-execute path for the StatusCodePages and ExceptionHandler middleware.
#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:

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-middlewareIncludes: URL rewrite, redirect, response cache/compression, session, and other general middlewaresarea-networkingIncludes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractionshelp candidateIndicates that the issues may be a good fit for community to help with. Requires work from eng. team

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions