ASP.NET Core Web API exception handling
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.
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 StartupConfigure(...)
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 :).
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.
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.