3

Reading the docs for UseExceptionHandler extension method, that adds standard exception handler middleware, I see following description:

UseExceptionHandler(IApplicationBuilder)

Adds a middleware to the pipeline that will catch exceptions, log them, and re-execute the request in an alternate pipeline. The request will not be re-executed if the response has already started.

However I was not able to find in docs what this means exactly. What is an alternate pipeline?

mlst
  • 2,688
  • 7
  • 27
  • 57

2 Answers2

1

You can see what the exception handler middleware does in the source: https://github.com/dotnet/aspnetcore/blob/240377059ec25b4d9d86d4188a26722e55edc5a1/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs#L111.

What I think they are referring to is that the middleware will reset the current HTTP context and run a new request through the middleware pipeline. At least that is if you provide the ExceptionHandlingPath to it.

juunas
  • 54,244
  • 13
  • 113
  • 149
1
  • In software-engineering in general, a "pipeline" refers to a sequence-of-events, steps, or handlers which are composed in advance, and which concerns the processing of something from start-to-finish, with the implication that, for each step, the output of a previous step is used as the input for the next step.
    • For example, a build-automation or CI/CD pipeline will be a predefined sequence of steps that take your project's repo, gather up its dependencies, invoke the compiler/linker/etc for whatever output targets there are, and publish the end-result, all in a single pipeline that's ideally hands-off.
    • Another example is in the Unix shell: where the stdout output of one command is fed as the stdin of another, e.g. makewords sentence | lowercase | sort | unique > output.txt

  • ASP.NET Core's concept of a pipeline refers to the composed sequence of middleware handlers (and other things) which you assemble during Startup with those UseSomething() methods on WebApplicationBuilder.

    • Prior to ASP.NET Core 6 this is what your Startup.Configure() method is for: it defines the sequence of handlers that get invoked for every incoming HTTP request.
      • (Don't confuse Configure() with ConfigureServices(): Configure() is for configuring the pipeline, while ConfigureServices() is for configuring the DI system: they're very distinct things.
      • If you're alarmed by this talk of configuring handlers that run for every request because it sounds wasteful or redundant - don't worry: ASP.NET Core has always supported branching the request pipeline, so not every middleware handler is necessarily invoked on every request. e.g. you want a different error-handler for requests under /api/* as opposed to /user-visible-HTML-pages/*
  • Speaking generally again (so not being specific to ASP.NET Core), a common technique for composing pipelines in any modern language that supports first-class functions and/or closures is to dynamcially compose them at runtime during an initialization phase. As a concrete example, let's look at what a statically composed (as opposed to dynamically composed) pipeline of functions would look like:

    // Implements the example Unix shell pipeline from Bell Labs' video:
    function myPipeline( sentence ) {
    
         return unique( sort( lowercase( makewords( sentence ) ) ) );
    }
    

    Now, to convert that myPipeline to use dynamic composition, those hard-coded functions need to be function-pointers, and their output still needs to be passed as input to the next function; to do this, modern libraries/frameworks will have you use a separate "builder" API/configuration-interface for composing the pipeline, so if we represented the above myPipeline using something like ASP.NET Core's Configure(), it would be something like this:

    function buildPipeline( builder )
    {
         return builder
             .use( makewords )
             .use( lowercase )
             .use( sort )
             .use( unique )
             .build();
    }
    

    The builder.Use() function would accept a function as a parameter and internally do something like this:

    function builder::use( func ) {
         this.steps.add( func );
         return this;
    }
    
    function builder::build() {
         return function( input ) {
             let prevOutput = input;
             foreach( const func of this.steps ) {
                 prevOutput = func( prevOutput );
             }
             retunr prevOutput;
         }
    }
    

    Notice that the build() function returns a closure (the return function( input ) { ... }), which captures its internal steps, which is the built pipeline. To "run" the pipeline you just need to invoke the returned function( input ) {...} and receive the result.

  • With that exposition out the way.... it then follows that when ASP.NET Core uses the term "alternative pipeline" means that there exists another HTTP request+response processing pipeline which will be used to try to complete a HTTP request which failed.

  • This is indeed the case: when you call .UseExceptionHandler(this IApplicationBuilder, ...), this is what happens

    • (Note that my sequence-of-events below does not correspond exactly to the sequence of instructions inside UseExceptionHandler):
    1. The UseExceptionHandler method takes your current (still-being-composed) pipeline and adds a new middleware step to it: ExceptionHandlerMiddleware (actually, it's ExceptionHandlerMiddlewareImpl).
      • So when an exception is thrown inside the pipeline while it's processing a request, it will be caught and presumably logged.
    2. But UseExceptionHandler also calls app.New() to create a brand new pipeline from scratch, which is initially empty - then it passes the new builder for this new pipeline back to the Action<IApplicationBuilder> configure callback so that the application-code can then add more middleware steps to it, if desired (presumably to show a user-friendly error message, etc).
    3. After the configure callback returns, UseExceptionHandler calls .Build() on that pipeline-builder (so now it's a baked/composed pipeline that's ready to handle requests), and then stores that pipeline in its ExceptionHandler field/property.
    4. So at runtime, when ExceptionHandlerMiddlewareImpl catches an exception, it aborts the rest of the current pipeline and instead passes the current HTTP request details to that ExceptionHandler property - which contains an entire separate pipeline, which then (hopefully!) runs to completion to build a response for the end-user/remote-client.
    5. Note that the design of UseExceptionHandler means it won't handle a second exception thrown in the secondary pipeline - for that reason, if you expect your exception-handling-pipeline to fail, you'll want to add another call to UseExceptionHandler inside its configure callback (though you probably shouldn't need to do this, as exception-handling code should not raise exceptions of its own...)
Dai
  • 141,631
  • 28
  • 261
  • 374