Translating Exceptions into Problem Details Responses

profile
Tim Deschryver
timdeschryver.dev

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, which returns a Bad Request (Results.BadRequest()) when an exception is catched. This results in a 400 Bad Request response.

While we understand what's happening, the response doesn't provide any information about the error. This isn't very helpful to the consumer(s) 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.

Note

Problem 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 as follows:

Besides the described format, this object can also be extended with additional members to provide more information, these are called Extension Members.

Problem Details in 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 to see a Problem Details response, including a helpful response body.

This only works for status codes that are returned by the application. To have the same behavior for exceptions, you also need to call the IApplicationBuilder.UseExceptionHandler() extension method.

This will translate the unhandled exception into a Problem Detail, and returns a 500 Internal Server Error response. Resulting in the following response when the application throws an unhandled exception.

We already have something better than the default behavior, but we can further 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 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() to the DI container.

The reasons why I find this better than the other options are that it's more explicit. It also provides more features such as injecting dependencies, short-circuiting the pipeline, and it can add multiple exception handlers to the middleware pipeline. Lastly, our handler receives the exception, which makes it easy to write logic based on it. This was also possible before, but to get ahold of the exception you had to grab the exception from the IExceptionHandlerFeature feature using HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error.

To write your own exception handler, create a new class that implements the IExceptionHandler interface.

Your IExceptionHandler implementation receives the HttpContext and the Exception as arguments in the TryHandleAsync method that you need to implement, 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 the next middleware in the pipeline is invoked.

A simple exception handler that returns a Problem Details response based on the thrown exception has the following implementation.

For all exceptions that are thrown within the application, the above 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 could use the exception type to return a different Problem Details response (including a different status code).

The last step is to register the exception handler using the IServiceCollection.AddExceptionHandler() extension method.

Now when an exception is thrown from an endpoint, we get a Problem Details response containing more information about the exception. This is because we append the exception message to the response as the detail of the Problem Details object.

Warning

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 sophisticated 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 to our availability, which creates a Problem Details response object more easily.

Keep in mind that the IProblemDetailsService service is available (and can be injected) because the IServiceCollection.AddProblemDetails() method is invoked, which registers the service to the DI container.

This means that we can use this to refactor our exception handler to use the IProblemDetailsService service! Instead of manually creating the problem details, we can do this by taking advantage of the service. 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:

Note

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.

Note

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.

Incoming links

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.

Buy Me a Coffee at ko-fi.com PayPal logo

Share this post