402

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 an 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 an exception occurs in my AuthorizationFilter it's not being handled by ErrorHandlingFilter. I was expecting it to be caught there just like it worked with the old ASP.NET Web API.

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

Grigory Zhadko
  • 1,484
  • 1
  • 19
  • 33
Andrei
  • 42,814
  • 35
  • 154
  • 218

12 Answers12

747

Quick and Easy Exception Handling

Simply add this middleware before ASP.NET routing into your middleware registrations.

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

Done!


Enable Dependency Injection for logging and other purposes

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:

[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 important notes and observations:

  • You can inject your dependencies into the Controller's constructor.
  • [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.

Here is the link to official Microsoft documentation.


Response model ideas.

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. With the described approach you have full flexibility and control over handling exceptions and returning the right response from your service.

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 from anywhere this way:

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

Then your handling code could be simplified to just this:

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

HttpContext.Features.Get<IExceptionHandlerFeature>() WAT?

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 in the request processing pipeline. Each middleware has access to request context and can write into the response if needed. Taking exception handling out of MVC makes sense if it's important to handle errors from non-MVC middlewares the same way as MVC exceptions, which I find is very common in real world apps. So because built-in exception handling middleware is not a part of MVC, MVC itself knows nothing about it and vice versa, exception handling middleware doesn't really know where the exception is coming from, besides of course it knows that it happened somewhere down the pipe of request execution. But both may needed to be "connected" with one another. So when exception is not caught anywhere, exception handling middleware catches it and re-runs the pipeline for a route, registered in it. This is how you can "pass" exception handling back to MVC with consistent content negotiation or some other middleware if you wish. The exception itself is extracted from the common middleware context. Looks funny but gets the job done :).

Andrei
  • 42,814
  • 35
  • 154
  • 218
  • 5
    I have been beating my head against the desk trying to get a custom middleware to work today, and it works basically the same way (I'm using it to manage unit of work/transaction for a request). The problem I'm facing is that raised exceptions in 'next' are not caught in the middleware. As you can imagine, this is problematic. What am I doing wrong/missing? Any pointers or suggestions? – brappleye3 Feb 17 '17 at 02:43
  • @brappleye3 first thought - make sure you `await` all your async calls e.g. `await context.SaveChangesAsync();`. It's hard to guess without seeing the code. I'd take a look if you post your question with details. – Andrei Feb 17 '17 at 14:01
  • 3
    I typically do a mix of both middleware and `IExceptionFilter`. The filter handles the controller errors directly, and I use the middleware for a more "low level" handling. As a hint, if someone needs to execute code by exception type in the global handler, to make it more "readable" feel free to give a look at a small library I made just for that: https://medium.com/@nogravity00/asp-net-core-mvc-and-exception-handling-f0da1c820d4a – João Simões Feb 17 '17 at 19:35
  • @brappleye3 @Andrei I'm having this same problem. I've used the code above for the middleware and registered it. Then I'm making an api call that just throws notimplemented. I can break on `await next(context);` but the break on `await HandleExceptionAsync(context, ex);`. Inspecting the context, the response has a 200 code. I'm a bit lost as to why as the exception has been thrown but not caught? Shouldn't this error be trapped here? – Jamadan Mar 27 '17 at 22:11
  • 10
    @brappleye3 - I figured out what the problem was. I was just registering the middleware in the wrong place in the Startup.cs class. I moved `app.UseMiddleware();` to just before `app.UseStaticFiles();`. The exception seems to be caught correctly now. This leads me to believe `app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); app.UseBrowserLink();` Do some internal magic middleware hackery to get the middleware ordering right. – Jamadan Mar 28 '17 at 10:36
  • This seems like it's only good for responding to "caught" exceptions. How can you have an API call to a bad URL return a JSON response with a 404 code rather than trying to redirect to a custom error page? – mellis481 Mar 30 '17 at 00:38
  • @im1dermike 404 is a bit different as it doesn't raise an exception, but you can always check `context.Response.StatusCode == 404` in exception handling middleware and react as needed. Other than that case, all the uncaught application exceptions will be caught in middleware. Hope it helps. – Andrei Mar 30 '17 at 01:41
  • `context.Response.StatusCode` at line `await next(context)` is 200 on a bad URL, not 404. – mellis481 Mar 30 '17 at 12:58
  • @im1dermike you should check it after `await next(context);` as MVC should execute first and set the status for you. – Andrei Mar 30 '17 at 13:08
  • It's still 200 after that line. – mellis481 Mar 30 '17 at 13:10
  • 4
    I agree that custom middleware can be very useful but would question using exceptions for NotFound, Unauthorised and BadRequest situations. Why not simply set the status code (using NotFound() etc.) and then handle it in your custom middleware or via UseStatusCodePagesWithReExecute? See https://www.devtrends.co.uk/blog/handling-errors-in-asp.net-core-web-api for more info – Paul Hiles May 25 '17 at 19:35
  • @Andrei When you say 'back to the top of the stack', it sounds as though your dependency graph is rather complex but in a typical application, using the out-of-the-box functionality is simpler and faster. On a high traffic site, you do not want to be throwing thousands of unnecessary exceptions. Yes NotFound() is a controller-only helper but the underlying NotFoundObjectResult can be used from anywhere. [Produces] and [SwaggerResponse] are the recommended methods for indicating return types for Swagger. – Paul Hiles May 26 '17 at 07:52
  • 1
    @PaulHiles if all your business logic is in controllers (which is ugly), sure, you can use `NotFound`, `BadRequest` etc. But for production projects even with simple n-tier design with just 3 layers - controllers, services and repositories it will be a huge pain. In some/most cases you can get away from it by returning `null` from service layer. Though `null` is a bad practice. There would be null-checks everywhere. Using exceptions is OK. No null-checks. Performance drop isn't significant and to be honest I'd sacrifice this performance for code-clarity any day. – Andrei May 26 '17 at 16:25
  • 1
    @Andrei Hmm OK. If it works for you. I personally find code that raises excepions for every day events, deep within the object graph much less clear. In a typical app, I would say first-level viewmodel validation and unauthorised checks can be handled by action filters or middleware without using exceptions. For 404's, the service can return an object with a status result and then the action can check and return NotFound(). For business level validation, it is more debatable. You can take the same approach as with 404s or use exceptions. – Paul Hiles May 27 '17 at 08:00
  • I think it is also worth reiterating that in response to your original question where you asked about how to capture all application exceptions as well as exceptions from action filters, the simplest way to do this is to use the built-in functionality rather than rolling your own - UseStatusCodePagesWithReExecute. – Paul Hiles May 27 '17 at 08:00
  • 1
    I use a very simillar approach myself. I would just like to add that this middleware will serialize to json by default `Uppercase` property names, while MVC will do it with `lowercase`. This is easily fixable just by adding a `JsonSerializerSettings` with a `CamelCasePropertyNamesContractResolver` to the `JsonConvert.SerializeObject(obj, settings)` call. – erikbozic Aug 01 '18 at 11:02
  • 4
    It's bad because it's always serializing to JSON, completely ignoring content negotiation. – Konrad Sep 18 '18 at 10:14
  • So even if you have some other content-type you expect the server to return it will return JSON anyway.... – Konrad Sep 18 '18 at 10:14
  • 9
    @Konrad valid point. That's why I said that this example is where you can get started, and not the end result. For 99% of APIs JSON is more than enough. If you feel like this answer isn't good enough, feel free to contribute. – Andrei Sep 18 '18 at 11:52
  • 1
    It is worth mentioning that some behavior has changed since ASP.NET Core 2.1 and the [ApiController] attribute, specifically the model validation errors (Bad Request). For more info: https://stackoverflow.com/a/51159755/189429 – Marcel de Castilho Feb 05 '19 at 23:37
  • 1
    There is a built-in middleware that we can use to write a bit less code (it will pretty much be equivalent to this answer): https://stackoverflow.com/a/55166404/1671558 – Ilya Chernomordik Mar 14 '19 at 15:39
  • 1
    Probably I'm suffering the "not invented here" syndrome - but I think your solution is a lot more clear and readable then the "out of the box" solution with UseExceptionHandler() – Dirk Boer Jun 06 '20 at 13:18
  • I just added an answer with a better way of doing this by relying upon UseExceptionHandler – r.pedrosa Jul 07 '20 at 15:22
  • it would be good that you at least mention me/my answer in your post. That being said, as you copying anyway, it would be good that you copy fully including [NonAction] annotation otherwise [web API analyses will give a compilation warning](https://learn.microsoft.com/en-us/aspnet/core/web-api/advanced/analyzers?view=aspnetcore-3.1&tabs=visual-studio#reference-the-analyzer-package) – r.pedrosa Jul 08 '20 at 08:28
  • that is why you completely change your answer 17 hours after I post mine to also use an ErrorController that extends built-in exception handling. 17 hours before, you had a solution two (equal to @Ilya Chernomordik answer...) although your still preferable solution was using your own middleware. **Those are facts, not opinions**. My answer is based on exception handling built-in functionality as many answers here but shows how can this be done in a better way. We can discuss that in my answer but I guess the advantages are clear to you otherwise you won't have your answer as it is now – r.pedrosa Jul 08 '20 at 14:38
  • @r.pedrosa it's all because you're da best, the most brilliant software engineer I've ever seen. Thank you so much - every line of your code is pure gold. – Andrei Jul 13 '20 at 19:35
  • 1
    "[ApiExplorerSettings(IgnoreApi = true)] is needed. Otherwise it may break your Swashbuckle swagger" ah thanks for mentioning this! Was going to crazy trying to figure out what broke the Swagger documentation. – mfcallahan Aug 14 '20 at 20:31
  • 1
    @Andrei Hey very generous post. the MS docs on this make you crazy. In the previous framework I could populate ReasonPhrase in my custom exception handler then use that in the UI app. What do I need to do, if you don't mind, to have HttpResponseMessage.ReasonPhrase populated with MySpecialString when the request returns to the caller? – pdschuller Jun 16 '22 at 02:32
  • @pdschuller thank you! Yes, this isn't obvious. Try this: `Response.HttpContext.Features.Get().ReasonPhrase = "Your reason phrase.";` with `using Microsoft.AspNetCore.Http.Features;` – Andrei Jun 16 '22 at 06:13
  • 1
    @Andrei Yep. I put that in the ErrorsController and I can send any message I want back to the web service method caller. Whew! – pdschuller Jun 20 '22 at 17:36
  • @pdschuller sweet! – Andrei Jun 24 '22 at 00:02
  • Great solution! I made something based on your concept. If you wanna replace hte context.Response StatusCode, you can put after before the `await context.Response.WriteAsJsonAsync(response);`: `if (exception is HttpStatusException httpException) context.Response.StatusCode = (int)((HttpStatusException)exception).Status;` – Fábio Jan 31 '23 at 17:33
128

There is a built-in middleware for that:

ASP.NET Core 5 version:

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

Older versions (they did not have 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 do pretty much the same, just a bit less code to write.

Important: Remember to add it before MapControllers \ UseMvc (or UseRouting in .Net Core 3) as order is important.

Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207
  • 2
    Does it support DI as an arg to the handler, or would one have to use a service locator pattern within the handler? – l p Apr 29 '20 at 02:16
  • Please check out accepted answer. With that approach you can use DI and you have full control over API response. – Andrei Feb 28 '21 at 12:56
35

Your best bet is to use middleware to achieve logging you're looking for. You want to put your exception logging in one middleware and then handle your error pages displayed to the user in a different middleware. That allows separation of logic and follows the design Microsoft has laid out with the 2 middleware components. Here's a good link to Microsoft's documentation: Error Handling in ASP.Net Core

For your specific example, you may want to use one of the extensions in the StatusCodePage middleware or roll your own like this.

You can find an example here for logging exceptions: 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 specific implementation, then you can also use ELM Middleware, and here are some examples: Elm Exception Middleware

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

If that doesn't work for your needs, you can always roll your own Middleware component by looking at their implementations of the ExceptionHandlerMiddleware and the ElmMiddleware to grasp the concepts for building your own.

It's important to add the exception handling middleware below the StatusCodePages middleware but above all your other middleware components. That way your Exception middleware will capture the exception, log it, then allow the request to proceed to the StatusCodePage middleware which will display the friendly error page to the user.

Steve Dunn
  • 21,044
  • 11
  • 62
  • 87
Ashley Lee
  • 3,810
  • 1
  • 18
  • 26
35

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

According to this link I got some idea to do the same. So I merged the Andrei Answer with this. So my final code is below:


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. Custom Exception Class Type

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. Custom Exception Middleware

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. Configure Method in startup.cs

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

Now my login method in Account controller :

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

Above you can see if i have not found the user then raising the HttpStatusCodeException in which i have passed HttpStatusCode.NotFound status and a custom message
In middleware

catch (HttpStatusCodeException ex)

blocked will be called which will pass control to

private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception) method


But what if i got runtime error before? 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

Task HandleExceptionAsync(HttpContext context, Exception exception)

method.

I have used a single ErrorDetails class for uniformity.

spottedmahn
  • 14,823
  • 13
  • 108
  • 178
ArjunArora
  • 986
  • 3
  • 12
  • 27
  • Where to put extension method? Unfortunately in the `startup.cs` in `void Configure(IapplicationBuilder app)` I get an error `IApplicationBuilder does not contain a definition for ConfigureCustomExceptionMiddleware`. And I added the reference, where `CustomExceptionMiddleware.cs` is. – Spedo De La Rossa Jun 04 '19 at 09:49
  • 3
    you don't want to use exceptions as they slow down your apis. exceptions are very expensive. – lnaie Oct 04 '19 at 12:04
  • 2
    @Inaie, Can't say about that... but it seems you have never got any exception to handle to.. Great work – ArjunArora Oct 05 '19 at 19:52
  • 3
    Are you sure to use "throw ex;" instead of "throw;" ? – Leszek P Jun 02 '21 at 10:30
  • @LeszekP, i think both will work, though i have not tested it – ArjunArora Jun 04 '21 at 07:59
  • I'm a little confused at the point of this answer, you do know the HTTP status code is already on the context right? `context.Response.StatusCode` – bytedev Jun 09 '22 at 09:51
  • @bytedev, I wanted to throw custom HTTP status code based on equation – ArjunArora Jun 16 '22 at 06:53
21

To Configure exception handling behavior per exception type you can use Middleware from NuGet packages:

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();
}
Ihar Yakimush
  • 562
  • 4
  • 6
20

Firstly, thanks to Andrei as I've based my solution on his example.

I'm including mine as it's a more complete sample and might save readers some time.

The limitation of Andrei's approach is that doesn't handle logging, capturing potentially useful request variables and content negotiation (it will always return JSON no matter what the client has requested - XML / plain text etc).

My approach is to use an ObjectResult which allows us to use the functionality baked into MVC.

This code also prevents caching of the response.

The error response has been decorated in such a way that it can be serialized by the XML serializer.

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate next;
    private readonly IActionResultExecutor<ObjectResult> executor;
    private readonly ILogger logger;
    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
    {
        this.next = next;
        this.executor = executor;
        logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));

            if (context.Response.HasStarted)
            {
                throw;
            }

            var routeData = context.GetRouteData() ?? new RouteData();

            ClearCacheHeaders(context.Response);

            var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

            var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
            {
                StatusCode = (int) HttpStatusCode.InternalServerError,
            };

            await executor.ExecuteAsync(actionContext, result);
        }
    }

    private static string GetRequestData(HttpContext context)
    {
        var sb = new StringBuilder();

        if (context.Request.HasFormContentType && context.Request.Form.Any())
        {
            sb.Append("Form variables:");
            foreach (var x in context.Request.Form)
            {
                sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
            }
        }

        sb.AppendLine("Method: " + context.Request.Method);

        return sb.ToString();
    }

    private static void ClearCacheHeaders(HttpResponse response)
    {
        response.Headers[HeaderNames.CacheControl] = "no-cache";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
        response.Headers.Remove(HeaderNames.ETag);
    }

    [DataContract(Name= "ErrorResponse")]
    public class ErrorResponse
    {
        [DataMember(Name = "Message")]
        public string Message { get; set; }

        public ErrorResponse(string message)
        {
            Message = message;
        }
    }
}
CountZero
  • 6,171
  • 3
  • 46
  • 59
  • See https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs if you want to check the current source code currently and add things from this approach. – perustaja Jan 04 '21 at 22:58
10

First, configure ASP.NET Core 2 Startup to re-execute to an error page for any errors from the web server and any unhandled exceptions.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment()) {
        // Debug config here...
    } else {
        app.UseStatusCodePagesWithReExecute("/Error");
        app.UseExceptionHandler("/Error");
    }
    // More config...
}

Next, define an exception type that will let you throw errors with HTTP status codes.

public class HttpException : Exception
{
    public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
    public HttpStatusCode StatusCode { get; private set; }
}

Finally, in your controller for the error page, customize the response based on the reason for the error and whether the response will be seen directly by an end user. This code assumes all API URLs start with /api/.

[AllowAnonymous]
public IActionResult Error()
{
    // Gets the status code from the exception or web server.
    var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
        httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;

    // For API errors, responds with just the status code (no page).
    if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
        return StatusCode((int)statusCode);

    // Creates a view model for a user-friendly error page.
    string text = null;
    switch (statusCode) {
        case HttpStatusCode.NotFound: text = "Page not found."; break;
        // Add more as desired.
    }
    return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}

ASP.NET Core will log the error detail for you to debug with, so a status code may be all you want to provide to a (potentially untrusted) requester. If you want to show more info, you can enhance HttpException to provide it. For API errors, you can put JSON-encoded error info in the message body by replacing return StatusCode... with return Json....

Edward Brey
  • 40,302
  • 20
  • 199
  • 253
5

Here is the official guideline from Microsoft covering WebAPI and MVC cases for all versions of .NET.

For Web API it suggests redirecting to a dedicated controller end-point to return ProblemDetails. As it may lead to potential exposure in the OpenAPI spec of end-points that aren't meant to be called directly, I'd suggest a simpler solution:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseExceptionHandler(a => a.Run(async context =>
    {
        var error = context.Features.Get<IExceptionHandlerFeature>().Error;
        var problem = new ProblemDetails { Title = "Critical Error"};
        if (error != null)
        {
            if (env.IsDevelopment())
            {
                problem.Title = error.Message;
                problem.Detail = error.StackTrace;
            }
            else
                problem.Detail = error.Message;
        }
        await context.Response.WriteAsJsonAsync(problem);
    }));
    ...
}

In this case, we leverage a standard middleware that returns custom details (with a stack trace for dev mode) and avoid creating 'internal' end-points.

P.S. Note that the official guideline relies on IExceptionHandlerPathFeature before .NET v3 and since then (up to v5 as for now) - on IExceptionHandlerFeature.

P.S.S. If you're throwing exceptions from the Domain layer to convert them to 4xx code, I'd suggest either using the khellang's ProblemDetailsMiddleware or returning DomainResult that can be later converted to IActionResult or IResult. The later option helps you to achieve the same result without the overhead of exceptions.

Alex Klaus
  • 8,168
  • 8
  • 71
  • 87
  • 2
    I like this because it's simple and seems to work--just add the code above and you have an instant global exception handler. Note: If you're using `app.UseDeveloperExceptionPage()`, don't forget to remove it for this and similar solutions to work. – Tawab Wakil Jul 02 '22 at 21:43
  • I noticed however that the exception handler was not invoked when throwing from a thread other than the main one. So for this case I'm using a simple try/catch in my new thread as a workaround (in order to log the exception). Maybe there is a better way. – Tawab Wakil Jul 03 '22 at 14:42
  • 2
    Just tested it and it DOES handle exceptions thrown from other threads (and I checked the `Thread.CurrentThread.ManagedThreadId`s for this claim). Your case is more likely to have another causation (e.g. an exception mapping middleware). Also, pay attention to the registering order of middlewares as emphasised in this [SO post](https://stackoverflow.com/a/66388483/968003). – Alex Klaus Jul 16 '22 at 09:23
4

By adding your own "Exception Handling Middleware", makes it hard to reuse some good built-in logic of Exception Handler like send an "RFC 7807-compliant payload to the client" when an error happens.

What I made was to extend built-in Exception handler outside of the Startup.cs class to handle custom exceptions or override the behavior of existing ones. For example, an ArgumentException and convert into BadRequest without changing the default behavior of other exceptions:

on the Startup.cs add:

app.UseExceptionHandler("/error");

and extend ErrorController.cs with something like this:

using System;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;

namespace Api.Controllers
{
    [ApiController]
    [ApiExplorerSettings(IgnoreApi = true)]
    [AllowAnonymous]
    public class ErrorController : ControllerBase
    {
        [Route("/error")]
        public IActionResult Error(
            [FromServices] IWebHostEnvironment webHostEnvironment)
        {
            var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
            var exceptionType = context.Error.GetType();
            
            if (exceptionType == typeof(ArgumentException)
                || exceptionType == typeof(ArgumentNullException)
                || exceptionType == typeof(ArgumentOutOfRangeException))
            {
                if (webHostEnvironment.IsDevelopment())
                {
                    return ValidationProblem(
                        context.Error.StackTrace,
                        title: context.Error.Message);
                }

                return ValidationProblem(context.Error.Message);
            }

            if (exceptionType == typeof(NotFoundException))
            {
                return NotFound(context.Error.Message);
            }

            if (webHostEnvironment.IsDevelopment())
            {
                return Problem(
                    context.Error.StackTrace,
                    title: context.Error.Message
                    );
            }
            
            return Problem();
        }
    }
}

Note that:

  1. NotFoundException is a custom exception and all you need to do is throw new NotFoundException(null); or throw new ArgumentException("Invalid argument.");
  2. You should not serve sensitive error information to clients. Serving errors is a security risk.
r.pedrosa
  • 709
  • 5
  • 12
  • I did this to return the same structure as netcore: var result = JsonSerializer.Serialize(new { errorCode = error.ErrorCode, errorDescription = error.ErrorDescription, }); There are some issues with it though, like e.g. TraceId – Ilya Chernomordik Jul 08 '20 at 15:00
  • @IlyaChernomordik I guess you are returning the ``result`` variable? As you can see in my code, I'm returning a built-in [``BaseController.ValidationProblem``](https://learn.microsoft.com/es-es/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.validationproblem?view=aspnetcore-2.2) or BaseController.Problem. HTTP 400 response ``` { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "detail": "File extension is not permitted.", "traceId": "|79eb7d85-40b4e4f64c19c86f.", "errors": {} } ``` – r.pedrosa Jul 08 '20 at 17:36
  • 1
    Yep, I know. It's a pain to generate it myself and to have e.g. TraceId right, which they change between versions additionally. So there is no way to use ValidationProblem in the middleware. I have the same problem with custom validation of headers: I'd like to return the response in exactly the same way, but since it's not used directly as a parameter I cannot use attribute validation, and in a middleware I would have to "emulate" ValidationProblem json myself... – Ilya Chernomordik Jul 08 '20 at 18:00
3

use middleware or IExceptionHandlerPathFeature is fine. there is another way in eshop

create a exceptionfilter and register it

public class HttpGlobalExceptionFilter : IExceptionFilter
{
  public void OnException(ExceptionContext context)
  {...}
}
services.AddMvc(options =>
{
  options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})
ws_
  • 1,076
  • 9
  • 18
  • Thanks a lot for your answer!!! You saved me!! I was implementing IActionFilter interface instead and it was not catching all the exceptions. Changing it to IExceptionFilter worked for me. Thanks a lot! – David Oganov Jun 23 '22 at 12:48
0

A simple way to handle an exception on any particular method is:

using Microsoft.AspNetCore.Http;
...

public ActionResult MyAPIMethod()
{
    try
    {
       var myObject = ... something;

       return Json(myObject);
    }
    catch (Exception ex)
    {
        Log.Error($"Error: {ex.Message}");
        return StatusCode(StatusCodes.Status500InternalServerError);
    }         
}
spottedmahn
  • 14,823
  • 13
  • 108
  • 178
Chris Halcrow
  • 28,994
  • 18
  • 176
  • 206
0

If you want set custom exception handling behavior for a specific controller, you can do so by overriding the controllers OnActionExecuted method.

Remember to set the ExceptionHandled property to true to disable default exception handling behavior.

Here is a sample from an api I'm writing, where I want to catch specific types of exceptions and return a json formatted result:

    private static readonly Type[] API_CATCH_EXCEPTIONS = new Type[]
    {
        typeof(InvalidOperationException),
        typeof(ValidationException)           
    };

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        base.OnActionExecuted(context);

        if (context.Exception != null)
        {
            var exType = context.Exception.GetType();
            if (API_CATCH_EXCEPTIONS.Any(type => exType == type || exType.IsSubclassOf(type)))
            {
                context.Result = Problem(detail: context.Exception.Message);
                context.ExceptionHandled = true;
            }
        }  
    }
vidmartin
  • 82
  • 5