One of the many benefits of working with JSON:API and GraphQL is having a standardize way to communicate failures to a client. If you are not working with a spec like JSON:API or GraphQL, then you are in the hands of the developer that built the API and every developers implements error handling differently.

Almost every HTTP API that I've consumed implements errors differently. Can we just agree to use Problem Details and be done with it?

— Derek Comartin (@codeopinion) April 11, 2021

RFC 7807, better known as “Problem Details for HTTP APIs” was created to standardize the way errors are handled in web APIs. Even though the RFC was published in 2016, it is not well-known, in fact, I myself did not know about it until 2019. To help spread the knowledge and usefulness of using “Problem Details” I would like to demonstrate how you can utilize it in .NET.

Problem details is currently going through a revision. You can make contributions here.

.NET already comes with a problem details class. No need to import extra packages. The problem details class has the following properties.

To demonstrate how to use RFC 7807 on .NET I will create a new dotnet web api project by running the following dotnet command.

dotnet new webapi -n ProblemDetailsExample

Within this new web api I will create a new exception handling middleware. The purpose of this middleware is to act as a mapper between an exception and a problem details json document. Basically, when an exception is caught by the middleware, it will extract metadata from the excepton to produce a Problem Details object.

Microsoft has some really good documentation on how to handle errors in .NET Web APIs, see Handle errors in ASP.NET Core web APIs.

Here is the middleware class definition without any mapping logic.

public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;

    public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception exception)
        {
            await HandleException(httpContext, exception);
        }
    }

    private Task HandleException(HttpContext httpContext, Exception exception)
    {
        
    }
}

When you create a new .NET web api it comes with a weather forecast controller, the controller returns an in-memoery list of weather forecast. I will use this controller to test my middleware, instead of returning a list of weather forecast, the GET() method will be changed to throw a new not implemented exception.

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        throw new NotImplementedException("This method has not been implemented.");
    }
}

To handle the GET() method throwing an exception I will need to modify the HandleException method within the middleware. The code below is the my first attempt at handling the exception.

private Task HandleException(HttpContext httpContext, Exception exception)
{
    var itle = exception.GetType().Name;
    var detail = exception.Message;
    var instance = httpContext.Request.GetDisplayUrl();

    var problemDetails = new ProblemDetails
    {
        Title = problemDetailsTitle,
        Detail = problemDetailsDetail,
        Status = 500,
        Type = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500",
        Instance = instance
    };

    httpContext.Response.StatusCode = 500;
    httpContext.Response.ContentType = "application/problem+json";
    return httpContext.Response.WriteAsJsonAsync(problemDetails);
}

As you can see the method extract information from the exception object. The exception name is used as the title, the exception message is used as the detail and the current request uri is used as the instance. Since this is just sample project, the type field will simply point to the MDN docs that corresponds to the HTTP status returned by the middleware. In your project, the type property should point to some documentation that provides addtional details.

The extension methods WriteAsJsonAsync and GetDisplayUrl are part of Microsoft.AspNetCore.Http.Extensions. The method WriteAsJsonAsync is only available in .NET 5 and above.

When an HTTP GET request is sent to /weatherforecast the not implemented exception is handled by the middleware, producing the following HTTP response.

{
  "type": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500",
  "title": "NotImplementedException",
  "status": 500,
  "detail": "This method has not been implemented.",
  "instance": "https://localhost:44336/WeatherForecast"
}

Great, I’m pretty happy with my first implementation, later on I’ll extend my implementation by introducing my own problem details type. For now I do want to make the title more human readable, to accomplish that I will rely upon Humanizer. Humanizer will tansform “NotImplemnetedException” into “Not Implemented Exception”. To install humanizer, run the following command.

Install-Package Humanizer.Core -Version 2.8.26

With humanizer now installed, I’ll modify the HandleException method again.

private Task HandleException(HttpContext httpContext, Exception exception)
{
    var title = exception.GetType().Name.Humanize(LetterCasing.Title);
    var detail = exception.Message;
    var instance = httpContext.Request.GetDisplayUrl();

    var problemDetails = new ProblemDetails
    {
        Title = title,
        Detail = detail,
        Status = 500,
        Type = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500",
        Instance = instance
    };

    httpContext.Response.StatusCode = 500;
    httpContext.Response.ContentType = ProblemDetailsContentType;
    return httpContext.Response.WriteAsJsonAsync(problemDetails);
}

Running the same HTTP request yields the following result.

{
  "type": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500",
  "title": "Not Implemented Exception",
  "status": 500,
  "detail": "This method has not been implemented.",
  "instance": "https://localhost:44336/WeatherForecast"
}

Much better.

I now want to extend the problem details class. I want to include additional types, and move the logic that extracts data from the exception into another class. This specialize problem details class can accept an expcetion as a parameter, the class would then create a problem details document out of the exception context. The idea is very similar to how the ValidationProblemDetails class works.

The class will be named ExceptionProblemDetails, for now it will have three constructors, the default constructor, one constructor that take an exception and a constructor that takes an exception and an httpcontext.

public class ExceptionProblemDetails : ProblemDetails
{
    public ExceptionProblemDetails()
    {

    }

    public ExceptionProblemDetails(Exception exception)
    {
        var title = exception.GetType().Name.Humanize(LetterCasing.Title);
        var message = exception.GetAllExceptions().Select(e => e.Message);
        var detail = exception.Message;

        Title = title;
        Detail = detail;
        Errors = new Dictionary<string, IEnumerable<string>>();
        Status = 500;
        Type = exception.HelpLink;
    }

    public ExceptionProblemDetails(Exception exception, HttpContext context)
    {
        var title = exception.GetType().Name.Humanize(LetterCasing.Title);
        var detail = exception.Message;
        var instance = context.Request.GetDisplayUrl();


        Title = exception.GetType().Name;
        Detail = detail;
        Instance = instance;
        Errors = new Dictionary<string, IEnumerable<string>>()
        {
            { "friendlyErrorMessage", new []{"Server encountered an error, please try again."} }
        };
        Status = context.Response.StatusCode;
        Type = exception.HelpLink;
    }

    public IDictionary<string, IEnumerable<string>> Errors { get; }
}

The first constructor doesn’t do anything, it assumes that the developer will fill all the property, this is an a good option for developer that prefer to their own style of metadata extraction. The second constructor just provides some good defaults i.e. 500 as the status code. The last constructor is where most of the exception metadata will be extracted. It simplifies the HandleException method.

 private static Task HandleException(HttpContext httpContext, Exception exception)
{
    var problemDetails = new ExceptionProblemDetails(exception, httpContext);

    httpContext.Response.StatusCode = (int) problemDetails.Status;
    httpContext.Response.ContentType = ProblemDetailsContentType;
    return httpContext.Response.WriteAsJsonAsync(problemDetails);
}

Here is response produces by the middleware now.

{
  "errors": {
    "friendlyErrorMessage": [
      "Server encountered an errorm, please try again."
    ]
  },
  "type": null,
  "title": "NotImplementedException",
  "status": 200,
  "detail": "This method has not been implemented.",
  "instance": "https://localhost:44336/WeatherForecast"
}

Perfect, though you should note that in your implementation, you should return a more detail error message, not just “Server encountered an error, please try again.”. I did that here in order to keep the example simple, it is up to your on how to implement the friendly error message, be that having a switch statment that handle each exception, perhaps having the content live in another API and then fetching it when the exception occurs, or you may try implementing your own exception class that provides all the required metadata.