ASP.NET Core Web API exception handling

Andrei picture Andrei · Jul 28, 2016 · Viewed 242.9k times · Source

I am using ASP.NET Core for my new REST API project after using regular ASP.NET Web API for many years. I don't see any good way to handle exceptions in ASP.NET Core Web API. I tried to implement exception handling filter/attribute:

public class ErrorHandlingFilter : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        HandleExceptionAsync(context);
        context.ExceptionHandled = true;
    }

    private static void HandleExceptionAsync(ExceptionContext context)
    {
        var exception = context.Exception;

        if (exception is MyNotFoundException)
            SetExceptionResult(context, exception, HttpStatusCode.NotFound);
        else if (exception is MyUnauthorizedException)
            SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
        else if (exception is MyException)
            SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
        else
            SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
    }

    private static void SetExceptionResult(
        ExceptionContext context, 
        Exception exception, 
        HttpStatusCode code)
    {
        context.Result = new JsonResult(new ApiResponse(exception))
        {
            StatusCode = (int)code
        };
    }
}

And here is my Startup filter registration:

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});

The issue I was having is that when exception occurres in my AuthorizationFilter it's not being handled by ErrorHandlingFilter. I was expecting it to be caught there just like it worked with old ASP.NET Web API.

So how can I catch all application exceptions as well as any exceptions from Action Filters?

Answer

Andrei picture Andrei · Aug 13, 2016

Use built-in Exception Handling Middleware

Step 1. In your startup, register your exception handling route:

// It should be one of your very first registrations
app.UseExceptionHandler("/error"); // Add this
app.UseEndpoints(endpoints => endpoints.MapControllers());

Step 2. Create controller that will handle all exceptions and produce error response:

[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorsController : ControllerBase
{
    [Route("error")]
    public MyErrorResponse Error()
    {
        var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
        var exception = context?.Error; // Your exception
        var code = 500; // Internal Server Error by default

        if      (exception is MyNotFoundException) code = 404; // Not Found
        else if (exception is MyUnauthException)   code = 401; // Unauthorized
        else if (exception is MyException)         code = 400; // Bad Request

        Response.StatusCode = code; // You can use HttpStatusCode enum instead

        return new MyErrorResponse(exception); // Your error model
    }
}

A few important notes and observations:

  • [ApiExplorerSettings(IgnoreApi = true)] is needed. Otherwise, it may break your Swashbuckle swagger
  • Again, app.UseExceptionHandler("/error"); has to be one of the very top registrations in your Startup Configure(...) method. It's probably safe to place it at the top of the method.
  • The path in app.UseExceptionHandler("/error") and in controller [Route("error")] should be the same, to allow the controller handle exceptions redirected from exception handler middleware.

Microsoft documentation for this subject is not that great but has some interesting ideas. I'll just leave the link here.

Response models and custom exceptions

Implement your own response model and exceptions. This example is just a good starting point. Every service would need to handle exceptions in its own way. But with this code, you have full flexibility and control over handling exceptions and returning a proper result to the caller.

An example of error response model (just to give you some ideas):

public class MyErrorResponse
{
    public string Type { get; set; }
    public string Message { get; set; }
    public string StackTrace { get; set; }

    public MyErrorResponse(Exception ex)
    {
        Type = ex.GetType().Name;
        Message = ex.Message;
        StackTrace = ex.ToString();
    }
}

For simpler services, you might want to implement http status code exception that would look like this:

public class HttpStatusException : Exception
{
    public HttpStatusCode Status { get; private set; }

    public HttpStatusException(HttpStatusCode status, string msg) : base(msg)
    {
        Status = status;
    }
}

This can be thrown like that:

throw new HttpStatusCodeException(HttpStatusCode.NotFound, "User not found");

Then your handling code could be simplified to:

if (exception is HttpStatusException httpException)
{
    code = (int) httpException.Status;
}

Why so un-obvious HttpContext.Features.Get<IExceptionHandlerFeature>()?

ASP.NET Core developers embraced the concept of middlewares where different aspects of functionality such as Auth, Mvc, Swagger etc. are separated and executed sequentially by processing the request and returning the response or passing the execution to the next middleware. With this architecture, MVC itself, for instance, would not be able to handle errors happening in Auth. So, they came up with exception handling middleware that catches all the exceptions happening in middlewares registered down in the pipeline, pushes exception data into HttpContext.Features, and re-runs the pipeline for specified route (/error), allowing any middleware to handle this exception, and the best way to handle it is by our Controllers to maintain proper content negotiation.