skip to Main Content

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 docs on what this means exactly and what is an alternate pipeline?

2

Answers


  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.

    Login or Signup to reply.
    • 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…)
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search