Coder Perfect

Exception handling in the ASP.NET Core Web API

Problem

After years of using traditional ASP.NET Web API, I’ve switched to ASP.NET Core for my new REST API project. In ASP.NET Core Web API, I don’t see any decent solution to handle exceptions. I attempted to implement the following filter/attribute for handling exceptions:

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
        };
    }
}

Here’s how I set up my Startup filter:

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

The problem I was having was that when an exception occurred in my AuthorizationFilter, ErrorHandlingFilter did not handle it. I expected it to be caught there the same way it was with the old ASP.NET Web API.

So, how do I catch all application exceptions as well as any Action Filter exceptions?

Asked by Andrei

Solution #1

Simply include this middleware in your middleware registrations before ASP.NET routing.

app.UseExceptionHandler(c => c.Run(async context =>
{
    var exception = context.Features
        .Get<IExceptionHandlerPathFeature>()
        .Error;
    var response = new { error = exception.Message };
    await context.Response.WriteAsJsonAsync(response);
}));
app.UseMvc(); // or .UseRouting() or .UseEndpoints()

Step 1: Create an exception handling route in your startup:

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

Step 2: Create a controller that will handle all exceptions and respond with an error:

[AllowAnonymous]
[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 key points and observations:

The official Microsoft documentation can be found here.

Create your own exceptions and response model. This is only a beginning point for you. Exceptions would have to be handled differently by each service. With the approach outlined, you have complete flexibility and control over how you handle errors and return the appropriate response from your service.

To give you some ideas, here’s an example of an error response model:

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();
    }
}

You could want to construct a http status code exception that looks like this for basic services:

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

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

This can be thrown from anyplace in the following manner:

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

Then your handling code could be reduced to the following:

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

HttpContext.Features.Get() WAT?

Different pieces of functionality, such as Auth, MVC, Swagger, and so on, are segregated and processed sequentially in the request processing pipeline by ASP.NET Core developers. Each middleware has access to the request context and, if necessary, can write to the response. If it’s vital to handle errors from non-MVC middlewares in the same way as MVC exceptions, which I find is quite common in real-world apps, taking exception handling out of MVC makes sense. Because built-in exception handling middleware is not part of MVC, MVC is unaware of it, and vice versa, exception handling middleware is unaware of where the exception is originating from, except from the fact that it occurred somewhere along the request execution pipeline.

Answered by Andrei

Solution #2

For this, there is a built-in middleware:

Version of ASP.NET Core 5:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;

    await context.Response.WriteAsJsonAsync(new { error = exception.Message });
}));

Previous versions (without the WriteAsJsonAsync extension):

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

It should essentially achieve the same thing, with a little less code to write.

Remember to put it before MapControllers UseMvc (or UseRouting in.Net Core 3) because the order matters.

Answered by Ilya Chernomordik

Solution #3

To get the logging you want, the best option is to use middleware. You want to place your exception logging in one middleware and then handle your user-facing error pages in another middleware. This enables for logic separation and adheres to Microsoft’s architecture for the two middleware components. Microsoft’s documentation can be found at this link: ASP.NET Core Error Handling

You might use one of the StatusCodePage middleware extensions or create your own like this for your specific scenario.

An example of logging exceptions may be found here: ExceptionHandlerMiddleware.cs

public void Configure(IApplicationBuilder app)
{
    // app.UseErrorPage(ErrorPageOptions.ShowAll);
    // app.UseStatusCodePages();
    // app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
    // app.UseStatusCodePages("text/plain", "Response, status code: {0}");
    // app.UseStatusCodePagesWithRedirects("~/errors/{0}");
    // app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
    // app.UseStatusCodePages(builder => builder.UseWelcomePage());
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");  // I use this version

    // Exception handling logging below
    app.UseExceptionHandler();
}

If you don’t like that particular implementation, you can also utilize ELM Middleware, as seen below: Middleware for Elm Exceptions

public void Configure(IApplicationBuilder app)
{
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");
    // Exception handling logging below
    app.UseElmCapture();
    app.UseElmPage();
}

If it doesn’t meet your requirements, you can always roll your own Middleware component by studying their implementations of the ExceptionHandlerMiddleware and ElmMiddleware to learn how to do so.

The exception handling middleware must be added after the StatusCodePages middleware but before any other middleware components. That way, your Exception middleware will catch the issue, report it, and then send the request to the StatusCodePage middleware, which will show the user a pleasant error page.

Answered by Ashley Lee

Solution #4

The well-accepted answer helped me a lot but I wanted to pass HttpStatusCode in my middleware to manage error status code at runtime.

I got some inspiration from this link to do the same. As a result, I combined Andrei’s answer with this. So here’s my final code:

1. Base class

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}

2. Type of Custom Exception Class

public class HttpStatusCodeException : Exception
{
    public HttpStatusCode StatusCode { get; set; }
    public string ContentType { get; set; } = @"text/plain";

    public HttpStatusCodeException(HttpStatusCode statusCode)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, string message) 
        : base(message)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner) 
        : this(statusCode, inner.ToString()) { }

    public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject) 
        : this(statusCode, errorObject.ToString())
    {
        this.ContentType = @"application/json";
    }

}

3. Middleware for Custom Exceptions

public class CustomExceptionMiddleware
{
    private readonly RequestDelegate next;

    public CustomExceptionMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (HttpStatusCodeException ex)
        {
            await HandleExceptionAsync(context, ex);
        }
        catch (Exception exceptionObj)
        {
            await HandleExceptionAsync(context, exceptionObj);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
    {
        string result = null;
        context.Response.ContentType = "application/json";
        if (exception is HttpStatusCodeException)
        {
            result = new ErrorDetails() 
            {
                Message = exception.Message,
                StatusCode = (int)exception.StatusCode 
            }.ToString();
            context.Response.StatusCode = (int)exception.StatusCode;
        }
        else
        {
            result = new ErrorDetails() 
            { 
                Message = "Runtime Error",
                StatusCode = (int)HttpStatusCode.BadRequest
            }.ToString();
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
        return context.Response.WriteAsync(result);
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        string result = new ErrorDetails() 
        { 
            Message = exception.Message,
            StatusCode = (int)HttpStatusCode.InternalServerError 
        }.ToString();
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return context.Response.WriteAsync(result);
    }
}

4. Extension Method

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
{
    app.UseMiddleware<CustomExceptionMiddleware>();
}

5. In startup.cs, configure the method.

app.ConfigureCustomExceptionMiddleware();
app.UseMvc();

Now, on the Account controller, I have the following login method:

try
{
    IRepository<UserMaster> obj 
        = new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
    var result = obj.Get()
        .AsQueryable()
        .Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower() 
            && sb.Password == objData.Password.ToEncrypt() 
            && sb.Status == (int)StatusType.Active)
        .FirstOrDefault();
    if (result != null)//User Found
        return result;
    else // Not Found
        throw new HttpStatusCodeException(HttpStatusCode.NotFound,
            "Please check username or password");
}
catch (Exception ex)
{
    throw ex;
}

You can see that if I don’t find the user, I raise the HttpStatusCodeException, which I’ve provided HttpStatusCode to. A custom message and a NotFound status In the realm of middleware,

blocked will be called, and control will be sent to

But what if I’ve previously encountered a runtime error? For that i have used try catch block which throw exception and will be catched in catch (Exception exceptionObj) block and will pass control to

method. For consistency, I’ve used a single ErrorDetails class.

Answered by Arjun

Solution #5

Middleware from NuGet packages can be used to configure exception handling behavior per exception type:

Code sample:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddExceptionHandlingPolicies(options =>
    {
        options.For<InitializationException>().Rethrow();

        options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();

        options.For<SomeBadRequestException>()
        .Response(e => 400)
            .Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
            .WithBody((req,sw, exception) =>
                {
                    byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
                    return sw.WriteAsync(array, 0, array.Length);
                })
        .NextPolicy();

        // Ensure that all exception types are handled by adding handler for generic exception at the end.
        options.For<Exception>()
        .Log(lo =>
            {
                lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
                lo.Category = (context, exception) => "MyCategory";
            })
        .Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
            .ClearCacheHeaders()
            .WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
        .Handled();
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseExceptionHandlingPolicies();
    app.UseMvc();
}

Answered by Ihar Yakimush

Post is based on https://stackoverflow.com/questions/38630076/asp-net-core-web-api-exception-handling