Handling Web API Exceptions with ProblemDetails middleware
Tags: aspnetcore, middleware, errors
Published: 2020-06-07 17:06:40

---
title: Handling Web API Exceptions with ProblemDetails middleware
published: true
date: 2020-05-19 10:00:00 UTC
tags: ASPNETCore,Middleware,ErrorHandling
canonical_url: https://andrewlock.net/handling-web-api-exceptions-with-problemdetails-middleware/
---

 ![Handling Web API Exceptions with ProblemDetails middleware](https://andrewlock.net/content/images/2020/problem_details_banner.png)

In this short post I describe [a handy error-handling middleware](https://github.com/khellang/Middleware/blob/25eac131b2595fa72e2072c87c24763e42bd8e31/src/ProblemDetails), created by [Kristian Hellang](https://twitter.com/khellang), that is used to return `ProblemDetails` results when an exception occurs.

## ProblemDetails and the [ApiController] attribute

ASP.NET Core 2.1 introduced the `[ApiController]` attribute which [applies a number of common API-specific conventions](https://docs.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-3.1#apicontroller-attribute) to controllers. In ASP.NET Core 2.2 an extra convention was added - transforming error status codes (>= 400) to `ProblemDetails`.

Returning a consistent type, `ProblemDetails`, for all errors makes it much easier for consuming clients. All errors from MVC controllers, whether they're a 400 (Bad Request) or a 404 (Not Found), return a `ProblemDetails` object:

![Problem details for NotFound requests](https://andrewlock.net/content/images/2020/problem_details_1.png)

However, if your application throws an exception, you don't get a `ProblemDetails` response:

![Developer exception page](https://andrewlock.net/content/images/2020/problem_details_2.png)

In the default `webapi` template (shown below), the developer exception page handles errors in the Development environment, producing the error above.

```csharp
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // Only add error handling in development environments
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}
```

In the production environment, there's no exception middleware registered so you get a "raw" 500 status code without a message body at all:

![Production exception page](https://andrewlock.net/content/images/2020/problem_details_3.png)

A better option would be to consistent, and return a `ProblemDetails` object for exceptions too. One way to achieve this would be to create a custom error handler, [as I described in a previous post](https://andrewlock.net/creating-a-custom-error-handler-middleware-function/). A better option is to use an existing NuGet package that handles it for you.

## ProblemDetailsMiddleware

The [ProblemDetailsMiddleware](https://github.com/khellang/Middleware/tree/master/src/ProblemDetails) from [Kristian Hellang](https://twitter.com/khellang) does exactly what you expect - it handles exceptions in your middleware pipeline, and converts them to `ProblemDetails`. It has a lot of configuration options (which I'll get to later), but out of the box it does exactly what we need.

Add the _Hellang.Middleware.ProblemDetails_ to your _.csproj_ file, by calling `dotnet add package Hellang.Middleware.ProblemDetails`. The latest version at the time of writing is 5.0.0:

```xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Hellang.Middleware.ProblemDetails" Version="5.0.0" />
  </ItemGroup>

</Project>
```

You need to add the required services to the DI container by calling `AddProblemDetails()`. Add the middleware itself to the pipeline by calling `UseProblemDetails`. You should add this early in the pipeline, to ensure it catches errors from any subsequent middleware:

```csharp
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddProblemDetails(); // Add the required services
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseProblemDetails(); // Add the middleware

        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}
```

With this simple addition, if you get an exception somewhere in the pipeline (in a controller for example), you'll still get a `ProblemDetails` response. In the Development environment, the middleware includes the exception details and the Stack Trace:

![Developer ProblemDetails page](https://andrewlock.net/content/images/2020/problem_details_4.png)

This is more than just calling `ToString()` on the `Exception` though - the response even includes the line that threw the exception (`contextCode`) and includes the source code before (`preContextCode`) and after (`postContextCode`) the offending lines:

![Context code](https://andrewlock.net/content/images/2020/problem_details_5.png)

In the Production environment, the middleware doesn't include these details for obvious reasons, and instead returns the basic `ProblemDetails` object only.

![Production ProblemDetails page](https://andrewlock.net/content/images/2020/problem_details_6.png)

As well as handling exceptions, the `ProblemDetailsMiddleware` also catches status code errors that come from _other_ middleware too. For example, if a request doesn't match any endpoints in your application, the pipeline will return a 404. The `ApiController` attribute won't catch that, so it _won't_ be converted to a `ProblemDetails` object.

Similarly, by default, if you send a `POST` request to a `GET` method, you'll get a `405` response, again _without_ a method body, _even if you apply the `[ApiController]` attribute_:

![Method not found does not cause a ProblemDetails response](https://andrewlock.net/content/images/2020/problem_details_7.png)

With the `ProblemDetailsMiddleware` in place, you get a `ProblemDetails` response for these error codes too:

![With the middleware in place, method not found causes a ProblemDetails response](https://andrewlock.net/content/images/2020/problem_details_8.png)

This behaviour gave exactly what I needed out-of-the-box, but you can also extensively customise the behaviour of the middleware if you need to. In the next section, I'll show some of these customization options.

## Customising the middleware behaviour

You can customise the behaviour of the `ProblemDetailsMiddleware` by providing a configuration lambda for an `ProblemDetailsOptions` instance in the `AddProblemDetails` call:

```csharp
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddProblemDetails(opts => {
        // configure here
    });
}
```

There's lots of possible configuration settings, as shown below. Most of the configuration settings are `Func<>` properties, that give access to the current `HttpContext`, and let you control how the middleware behaves.

```csharp
public class ProblemDetailsOptions
{
    public int SourceCodeLineCount { get; set; }
    public IFileProvider FileProvider { get; set; }
    public Func<HttpContext, string> GetTraceId { get; set; }
    public Func<HttpContext, Exception, bool> IncludeExceptionDetails { get; set; }
    public Func<HttpContext, bool> IsProblem { get; set; }
    public Func<HttpContext, MvcProblemDetails> MapStatusCode { get; set; }
    public Action<HttpContext, MvcProblemDetails> OnBeforeWriteDetails { get; set; }
    public Func<HttpContext, Exception, MvcProblemDetails, bool> ShouldLogUnhandledException { get; set; }
}
```

For example, by default, `ExceptionDetails` are included only for the Development environment. If you wanted to include the details in the Staging environment too, you could use something like the following:

```csharp
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddProblemDetails(opts =>
    {
        // Control when an exception is included
        opts.IncludeExceptionDetails = (ctx, ex) =>
        {
            // Fetch services from HttpContext.RequestServices
            var env = ctx.RequestServices.GetRequiredService<IHostEnvironment>();
            return env.IsDevelopment() || env.IsStaging();
        };
    });
}
```

Another thing worth pointing out is that you can control when the middleware should convert non-exception responses to `ProblemDetails`. The default configuration converts non-exception responses to `ProblemDetails` when the following is true:

- The status code is between 400 and 600.
- The `Content-Length` header is empty.
- The `Content-Type` header is empty.

As I mentioned at the start of this post, the `[ApiController]` attribute from ASP.NET Core 2.2 onwards automatically converts "raw" status code results into `ProblemDetails` anyway. Those responses are ignored by the middleware, as the response will already have a `Content-Type`.

However, if you're _not_ using the `[ApiController]` attribute, or are still using ASP.NET Core 2.1, then you can use the `ProblemDetailsMiddleware` to automatically convert raw status code results into `ProblemDetails`, just as you get in ASP.NET Core 2.2+.

> The responses in these cases aren't _identical_, but they're very similar. There are small differences in the values used for the `Title` and `Type` properties for example.

Another option would be to use the `ProblemDetailsMiddleware` in an application that combines Razor Pages with API controllers. You could then use the `IsProblem` function to ensure that `ProblemDetails` are only generated for API controller endpoints.

I've only touched on a couple of the customisation features, but there's lots of additional hooks you can use to control how the middleware works. I just haven't had to use them, as the defaults do exactly what I need!

## Summary

In this post I described the `ProblemDetailsMiddleware` by [Kristian Hellang](https://twitter.com/khellang), that can be used with API projects to generate `ProblemDetails` results for exceptions. This is a very handy library if you're building APIs, as it ensures all errors return a consistent object. The project is [open source on GitHub](https://github.com/khellang/Middleware/blob/25eac131b2595fa72e2072c87c24763e42bd8e31/src/ProblemDetails), and available [on NuGet](https://www.nuget.org/packages/Hellang.Middleware.ProblemDetails/), so check it out!