ASP.NET Core Web API exception handling
I started using ASP.NET Core for my new REST API project after using regular ASP.NET Web API for many years. I don't see a 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 occurred 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?
Exception Handling Middleware
After many experiments with different exception handling approaches I ended up using middleware. It worked the best for my ASP.NET Core Web API application. It handles application exceptions as well as exceptions from filters and I have full control over exception handling and creating a response json. Here is my exception handling middleware:
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate next;
public ErrorHandlingMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var code = HttpStatusCode.InternalServerError; // 500 if unexpected
if (exception is MyNotFoundException) code = HttpStatusCode.NotFound;
else if (exception is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
else if (exception is MyException) code = HttpStatusCode.BadRequest;
var result = JsonConvert.SerializeObject(new { error = exception.Message });
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)code;
return context.Response.WriteAsync(result);
}
}
Register it before MVC in Startup
class:
app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();
Here is an example of exception response:
{ "error": "Authentication token is not valid." }
You can add stack trace, exception type name, error codes or anything you want to it. Very flexible. Hope it's a good starting point for you!
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 componenets. 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.
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...
.