question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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

See original GitHub issue

Issue

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:

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:open
  • Created 2 years ago
  • Reactions:5
  • Comments:10 (9 by maintainers)

github_iconTop GitHub Comments

1reaction
Tratchercommented, Mar 22, 2022

A few more thoughts about the design first: Instead of Func<HttpContext, Task<string>> returning the new path, how about Func<HttpContext, Task> where you are responsible for updating the HttpContext directly? I’d also consider something like Func<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?

0reactions
seangwrightcommented, Dec 8, 2022

@MadL1me Go for it! Ping me on a PR when you create one.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Handle errors in ASP.NET Core
This exception handling middleware: Catches and logs unhandled exceptions. Re-executes the request in an alternate pipeline using the path ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found