Mapping exceptions to HTTP responses with .NET Core

Handling exceptions and returning the appropriate response to the application consuming your API or the user browsing your web application, is considered a best practice when developing software. Handling exceptions with .NET Core is as easy as pie. You can read all about it here. However, there are still a few downsides:

  • All types of exceptions are redirected to the same controller action and thus result in the same response.
  • All types of exceptions result in a HTTP 500 Internal Server Error.
  • Cross referencing logged exceptions with reported ones is still a pain.

What to do if you are building a JSON REST API? Always return the same kind of response with the same HTTP status code? Doesn't sound like a good idea, giving the consuming application a proper response with an error code and a message describing the error is probably a better solution. So what if we could map an exception to a HTTP response?

Going down the road of middleware

To be able todo this, I figured I could create some middleware that I can hook up to my pipeline to handle it like so:

app.UseResponseExceptionHandler(options =>  
{
    options.Map<ArgumentNullException>(HttpStatusCode.BadRequest);
    options.Map<KeyNotFoundException>(HttpStatusCode.NotFound, 
        "The requested resource was not found.");
    options.Map<CustomException>(HttpStatusCode.NotAcceptable, 
        new CustomResponse { Error = "Custom error" });
});

The middleware has three ways of mapping exceptions to HTTP responses:

1. HTTP status code only

When only mapping a HTTP status code to an exception, the exception message is used in the response:

HTTP/1.1 400 Bad Request  
Content-Type: application/json

{"message":"Value cannot be null. Parameter name: param"}

2. HTTP status code with an error message.

When mapping a HTTP status code with an error message, the given error message will be used in the response:

HTTP/1.1 404 Not Found  
Content-Type: application/json

{"message":"The requested resource was not found."}

3. HTTP status code with an object.

When mapping a HTTP status code with an object, the specified object will be serialized and put in the response:

HTTP/1.1 406 Not Acceptable  
Content-Type: application/json

{"error":"Custom error"}

Unhandled exceptions

But what about exceptions we don't map? Exceptions that are not mapped will be treated as what you can call 'unhandled'. Because they are not expected to occur, a random error code is generated, logged and put in the response for easy cross referencing:

HTTP/1.1 500 Internal Server Error  
Content-Type: application/json

{
    "errorCode":"ERR_40A7DD58",
    "message":"An unhandled exception has occurred, please check the log for details."
}

The error code is default prefixed with ERR_, but is overridable through the options parameter as well as the default error message:

app.UseResponseExceptionHandler(options =>  
{
    options.ErrorCodePrefix = "ERROR_";
    options.DefaultErrorMessage = "Unknown exception, please contact the System Administrator";
});

The JSON serializer settings can also be modified by using the options parameter.

Putting it all together

To prevent your code from turning into spaghetti code, you should consider moving away from exceptions and program in a more functional way by implementing Result and Maybe objects to avoid having to throw exceptions yourself. However, exceptions like an ArgumentNullException will most likely keep existing somewhere in your code. Logging error codes and outputting them makes tracking bugs and providing feedback to end users a lot faster, resulting in better code and making users more happy. In those cases, this middleware might come in handy.

Check out the full source at GitHub.