Translating Exceptions into Problem Details Responses
Because (ASP).NET 8 is in preview it doesn't mean we can't start trying out some of the new features.
In this post, we'll take a look at the new IExceptionHandler
interface, which is introduced in Preview 5, to turn exceptions into Problem Details.
Default API behavior link
Before we start introducing problem details and the exception handler, let's refresh our memory on how ASP.NET Core handles exceptions by default. An important detail to note is that the default behavior is different between development and production environments. Throughout this post, I'm only going to focus on the production environment.
When an endpoint is invoked and an exception is thrown while the request is being processed, the endpoint returns a 500 Internal Server Error response.
This is different when the exception is caught, and handled by the developer.
An example of this is that the logic within the endpoint is wrapped in a try-catch block, and the catch block returns a Bad Request (Results.BadRequest()
).
This results in a 400 Bad Request response.
These responses are not very helpful for the consumer of the API.
Problem Details link
Problem Details is a described format for returning error information in HTTP API responses. While it's still in the "Proposed Standard" status, it's already widely used in the industry and built into ASP.NET Core.
format_quoteProblem Details for HTTP APIs defines a "problem detail" as a way to carry machine- readable details of errors in an HTTP response to avoid the need to define new error response formats for HTTP APIs.
The Problem Details format looks like this:
Besides the described format, this object can also be extended with additional members to provide more information, these are called "Extension Members".
Problem Details with ASP.NET link
ASP.NET Core already has built-in support for Problem Details.
To enable it, you need to invoke the IServiceCollection.AddProblemDetails()
extension method and the IApplicationBuilder.UseStatusCodePages()
extension method.
Now, when we invoke an endpoint that returns a non-successful status code, we get a Problem Details response, including a response body.
To have the same behavior for exceptions, you also need to call the IApplicationBuilder.UseExceptionHandler()
extension method.
Resulting in the following response when the application throws an unhandled exception.
The result is already better than the default behavior, but we can improve this.
Exception Handler link
Before the addition of IExceptionHandler
exception handler, we could make use of a Exception Handler Page or a Exception Handler Lambda. Both of these options are still available, but the new IExceptionHandler
interface provides a more clean and flexible way of handling exceptions in my opinion.
An exception handler is a class that implements the IExceptionHandler
interface, and is registered by using IServiceCollection.AddExceptionHandler()
.
The reasons why I find this better than the other options are that it's more explicit.
It can also inject dependencies, short-circuit the pipeline or it can create a chain of multiple exception handlers, and the handler has access to the exception (previously you could get ahold of the exception with HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error
).
To write your own exception handler, you need to implement the IExceptionHandler
interface, which looks as follows:
Your custom TryHandleAsync
implementation receives the HttpContext
and the Exception
, and needs to return a ValueTask<bool>
.
If the handler returns true
, the exception is considered handled, and the request pipeline is short-circuited.
When the value false
is returned, the default flow continues and a possible next exception handler will be invoked.
A simple implementation of an exception handler that returns a Problem Details response based on the thrown exception could look like this.
For all exceptions that are thrown within the application, this handler will be invoked.
Let's go over it to see what it does. The handler sets the HTTP response status code, and creates and writes a Problem Details response object directly to the response using the information from the exception.
A more advanced implementation would use the exception type to return a different Problem Details response (and status code) for specific exception types.
The last step before this handler can be used is to register the exception handler using the IServiceCollection.AddExceptionHandler()
extension method.
When we invoke an endpoint that throws an exception, we get a Problem Details response containing more information about the exception.
An important detail to note is that you should be careful with the information you return in the response. You don't want to leak sensitive information to the consumer of the API. This can be in the form of technical information, or business information.
Exception Handler using IProblemDetailsService
link
In the previous example, we created a Problem Details response manually. While this works, there's a more suffocated solution available to return a Problem Details response.
Do you remember that I mentioned that ASP.NET has built-in support for Problem Details?
It doesn't only mean that problem details are returned for non-successful responses, nor does it mean that the ProblemDetails
class exists in the framework.
It also means that there's the IProblemDetailsService
service, which is available and can be used to create a Problem Details response object.
The service is provided when the IServiceCollection.AddProblemDetails()
method is invoked.
This means that we can refactor our exception handler to use the IProblemDetailsService
service to create the Problem Details response.
The cleaned-up version of the exception handler looks like this.
After this change, the response is still the same as before.
But, if you have a sharp eye, you might have noticed several small differences.
Because we use the IProblemDetailsService
service:
- the content type is now
application/problem+json
instead ofapplication/json
; - we're not required to set the
Status
property on theProblemDetails
instance because theIProblemDetailsService
service will set it for us using the status code of the response;
Instead of using the default Problem Details services you can also write your own implementation of IProblemDetailsWriter, but I haven't found a good use-case for this yet. If you have one, please let me know!
Default Problem Detail Extensions link
I also mentioned that problem details can be extended with additional properties.
One way of doing this is by providing these members during the instantiation of the ProblemDetails
instance.
But, we can also configure the IProblemDetailsService
service to add default properties that we want to add to every Problem Details response.
For this, we can use the overload of the AddProblemDetails()
method.
In the example below, we add the trace identifier and the instance (the endpoint that is invoked) to the Problem Details response.
Resulting in the following response object when an exception is thrown.
Conclusion link
In this post, I implemented the ExceptionToProblemDetailsHandler
class that implements the newly introduced IExceptionHandler
interface in ASP.NET Core 8.
The exception handler uses the Problem Details Service to translate thrown exceptions into Problem Details response objects.
The result is a standardized and better experience for your API consumers when the request is invalid, or when something went wrong during the processing of the request.
My Microsoft Learn Collection contains the resources that I used to learn these error handling features in ASP.NET. The resources also contain more information about the other error-handling features.
Feel free to update this blog post on GitHub, thanks in advance!
Join My Newsletter (WIP)
Join my weekly newsletter to receive my latest blog posts and bits, directly in your inbox.
Support me
I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.