Disable *all* exception handling in ASP.NET Web API 2 (to make room for my own)?

I want to wire up exception handling in a middleware component, something like this:

public override async Task Invoke(IOwinContext context)
{
    try
    {
        await Next.Invoke(context);
    }
    catch (Exception ex)
    {
        // Log error and return 500 response
    }
}

However, some of the exceptions I would like to catch are being caught and converted to HttpErrorResponses by the Web API pipeline before I can get to them. In the process, I lose a lot of details about the errors, so I can't get useful stack traces when debugging etc (the debugger doesn't even stop when the exception is thrown - I have to manually step through the code and see where it fails...).

I tried adding a custom exception handler with the following implementation:

public Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken)
{
    var owinContext = context.Request.GetOwinContext();
    owinContext.Set(Constants.ContextKeys.Exception, context.Exception);
    return Task.FromResult(0);
}

registered through config.Services.Replace(typeof(IExceptionHandler), new MyExceptionHandler()); in my startup configuration, but looking at it after executing Next.Invoke(context) through

context.Get<Exception>(Constants.ContextKeys.Exception);

still doesn't give me all the detail I want, as well as failing to stop at the fault point with the debugger.

Is there a way I can completely turn off all built-in error handling, so that my own middleware can take care of it?

Clarification, since a lot of people seem to misunderstand what I'm after:

  • The built-in error handling in Web API catches some (but not all) exceptions and rewrites them into 500 responses.
  • I want to catch all exceptions, do some logging, and then emit 500 responses with the information I choose (for most of them, see next bullet).
  • There are also some exceptions that signal business logic faults, for which I want to return 40x errors instead.
  • I want this to be at the top of the (app) pipeline, i.e. wrapping everything else in the request lifecycle
  • I want to handle this using OWIN, to make it portable to a possible future self-hosted scenario (i.e. it's not written in stone that this app will always be hosted on IIS - HTTP modules, Global.asax.cs et al are not relevant here).

Solution 1:

Update: I blogged about this. When researching the blog post, I found some potential for improvement; I've updated the relevant parts of this answer. For more detail on why I think this is better than all other suggestions here, or the default behavior, read the entire post :)


I have now gone with the following approach, which seems to work OK, even if not 100 % compliant with what I was looking for:

  • Create a class PassthroughExceptionHandler:

    public class PassthroughExceptionHandler : IExceptionHandler
    {
        public Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken)
        {
            // don't just throw the exception; that will ruin the stack trace
            var info = ExceptionDispatchInfo.Capture(context.Exception);
            info.Throw();
            return Task.CompletedTask;
        }
    }
    
  • Let that class replace the IExceptionHandler service of Web API:

    config.Services.Replace(typeof(IExceptionHandler), new PassthroughExceptionHandler());
    
  • Create a middleware class which does what I want:

    public class ExceptionHandlerMiddleware
    {
        public override async Task Invoke(IOwinContext context)
        {
            try
            {
                await Next?.Invoke(context);
            }
            catch (Exception ex)
            {
                // handle and/or log
            }
        }
    }
    
  • Register that middleware first in the stack:

    app.Use<ExceptionHandlerMiddleware>()
       .UseStageMarker(PipelineStage.Authenticate)
       // other middlewares omitted for brevity
       .UseStageMarker(PipelineStage.PreHandlerExecute)
       .UseWebApi(config);
    

I will still award the bounty to anyone who comes up with (bounty expired...) I'm still looking for a better solution, which, for example, breaks when an unhandled exception is thrown. (This approach makes VS break when I rethrow the exception in the handler, but the original call stack is lost; I have to set a breakpoint at the faulting line and debug again to be able to intercept the state when an exception is thrown.)

Solution 2:

Not sure if this will work for you, but I have a similar requirement to send all errors back as JSON even for not found errors. I created a base controller and overrode the ExecuteAsync allowing me to create my own responses.

public class ControllerBase : ApiController
{
    protected string ClassName = "ControllerBase::";

    public override System.Threading.Tasks.Task<HttpResponseMessage> ExecuteAsync(System.Web.Http.Controllers.HttpControllerContext controllerContext, System.Threading.CancellationToken cancellationToken)
    {
        try
        {
            System.Threading.Tasks.Task<HttpResponseMessage> TaskList = base.ExecuteAsync(controllerContext, cancellationToken);

            if (TaskList.Exception != null && TaskList.Exception.GetBaseException() != null)
            {
                JSONErrorResponse AsyncError = new JSONErrorResponse();
                AsyncError.ExceptionMessage = TaskList.Exception.GetBaseException().Message;
                AsyncError.ErrorMessage = string.Format("Unknown error {0} ExecuteAsync {1}", ClassName ,controllerContext.Request.RequestUri.AbsolutePath);
                AsyncError.HttpErrorCode = HttpStatusCode.BadRequest;

                HttpResponseMessage ErrorResponse = controllerContext.Request.CreateResponse(AsyncError.HttpErrorCode, AsyncError);

                return System.Threading.Tasks.Task.Run<HttpResponseMessage>(() => ErrorResponse);
            }
            return TaskList;
        }
        catch (Exception Error)
        {
            JSONErrorResponse BadParameters = new JSONErrorResponse();
            BadParameters.ExceptionMessage = Error.Message;
            BadParameters.ErrorMessage = string.Format("Method [{0}], or URL [{1}] not found, verify your request", controllerContext.Request.Method.Method, controllerContext.Request.RequestUri.AbsolutePath);
            BadParameters.HttpErrorCode = HttpStatusCode.NotFound;
            HttpResponseMessage ErrorResponse = controllerContext.Request.CreateResponse(BadParameters.HttpErrorCode, BadParameters);

            return System.Threading.Tasks.Task.Run<HttpResponseMessage>(() => ErrorResponse);
        }
    }
}

public class JSONErrorResponse
{
    //Possible message from exception
    public string ExceptionMessage { get; set; }
    //Possible custom error message
    public string ErrorMessage { get; set; }
    //Http error code
    public HttpStatusCode HttpErrorCode { get; set; }
}