Implementing Problem Details in ASP.NET Core APIs

You are currently viewing Implementing Problem Details in ASP.NET Core APIs
Implementing Problem Details in ASP.NET Core APIs

One of the most overlooked aspects when building APIs is how they communicate errors. A well-designed API informs consumers effectively when something goes wrong. Consistent error responses are not just a best practice but a necessity. They help developers troubleshoot issues faster, provide a better developer experience, and create a foundation of trust in your API. By adopting standardized error responses, you make your API predictable, professional, and user-friendly. In this article, we will learn how the Problem Details standard can help us in structuring error responses in ASP.NET Core Web APIs and make it easier for clients to understand and handle API errors.

What is Problem Details?

The standard “Problem Details for HTTP APIs” was first introduced by RFC 7807 and then recently refined in RFC 9457. It defines a set of rules for expressing error details in a structured, consistent, and machine-readable JSON (and XML) format. The objective of this standard is to make error response more informative and actionable, not just for human developers, but for the systems that consume APIs at runtime.

Problem Details

The Problem Details object structure contains the following fields.

  • type: A URI reference that identifies the problem type. It’s intended to provide human operators with a place to find more information about the error. If not present or applicable, it’s assumed to be “about:blank”.
  • title: A short, human-readable summary of the problem type. It should not change from occurrence to occurrence of the problem, except for purposes of localization.
  • status: The HTTP status code generated by the origin server for this occurrence of the problem.
  • detail: A human-readable explanation specific to this occurrence of the problem. Unlike the title, this field’s content can vary by occurrence.
  • instance: A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced.

Benefits of using Problem Details

Following are some of the benefits of using the Problem Details.

  • Improved Developer Experience: It enhances developer experience by providing clear, standardized information about issues, making APIs easier to integrate and maintain.
  • Improved Debugging: Provides detailed, structured information about errors, simplifying troubleshooting for both API developers and consumers.
  • Reduced Development Time and Cost: Standardizes error responses across APIs, making it easier for developers to predict and handle errors more quickly. Developers spend excessive time deciphering errors and implementing custom handlers for each API which decreases overall costs associated with ingesting an API as well as ongoing maintenance.
  • Better Client Experience: Enables client applications to programmatically interpret and respond to errors with minimal ambiguity.
  • Extensibility: This can be customized to include additional fields like error codes or trace IDs for more context.
  • Compliance with Standards: Aligns APIs with established best practices (RFC 9457), fostering interoperability and credibility.
READ ALSO:  Decorator Design Pattern in ASP.NET Core 5

Configuring Problem Details in ASP.NET Core

Let’s create an ASP.NET Core Web API project with the following controller and action method that is throwing a fictitious unhandled exception.

[ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
    public IActionResult Get()
    {
        throw new Exception("Product Not Found.");
    }
}

If you will run the project, you will see the following default exception page.

Default Exception Page in ASP.NET 9

ASP.NET Core has built-in support for Problem Details, which can be enabled in the Program.cs file by calling the AddProblemDetails method. This method registers the problem details middleware that will handle exceptions and return a problem details response.

builder.Services.AddProblemDetails();

We also need to enable another middleware using the UseExceptionHandler method that automatically converts unhandled exceptions into Problem Details responses.

app.UseExceptionHandler();

Now run the same API again and this time you will notice that the unhandled exception is automatically translated to a Problem Details response:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
  "title": "An error occurred while processing your request.",
  "status": 500,
  "traceId": "00-3d4076ecea61224816c7f595c90d2897-7514c47216448cda-00"
}

Finally, you can call a UseStatusCodePages method to add a middleware that will return a problem details response for common HTTP status codes.

app.UseStatusCodePages();

Now try to access any Web API that doesn’t exist by typing some random URL and you will see the following Problem Details response generated automatically.

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404
}

Global Handling of Problem Details using IExceptionHandler

If we have a large application, then it’s a good practice to handle all unhandled exceptions globally in one place. The global exception handler should not only catch all unhandled exceptions but should also map each exception to the correct problem details response before returning it. A new IExceptionHandler interface introduced in .NET 8 makes it very easy to implement such a global exception handler.

public class CustomExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, 
        Exception exception, CancellationToken cancellationToken)
    {
        var statusCode = exception switch
        {
            BadHttpRequestException => StatusCodes.Status400BadRequest,
            UnauthorizedAccessException => StatusCodes.Status401Unauthorized,
            _ => StatusCodes.Status500InternalServerError
        };

        var problemDetails = new ProblemDetails
        {
            Title = statusCode == StatusCodes.Status500InternalServerError
                ? "Internal Server Error"
                : "A handled exception occurred",
            Status = statusCode,
            Type = exception?.GetType().Name,
            Detail = exception?.Message,
            Instance = httpContext.Request.Path
        };

        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        return true;
    }
}

The above custom exception handler has defined the TryHandleAsync method that will be invoked by the problem details middleware when any exception is thrown.

READ ALSO:  A Beginner's Guide to GraphQL

Next, we are mapping different exceptions such as BadHttpRequestException and UnauthorizedAccessException with appropriate HTTP status codes using the C# new switch statement syntax. We can map more specific exceptions in the same way otherwise we can map all remaining exceptions with status code 500 – Internal server error we did in our handler.

var statusCode = exception switch
{
    BadHttpRequestException => StatusCodes.Status400BadRequest,
    UnauthorizedAccessException => StatusCodes.Status401Unauthorized,
    _ => StatusCodes.Status500InternalServerError
};

Finally, we created a ProblemDetails() object with the title, status code, and exception details and returned that by writing it to the response.

var problemDetails = new ProblemDetails
{
    Title = statusCode == StatusCodes.Status500InternalServerError
        ? "Internal Server Error"
        : "A handled exception occurred",
    Status = statusCode,
    Type = exception?.GetType().Name,
    Detail = exception?.Message,
    Instance = httpContext.Request.Path
};

Notice also that we return true at the end of the method, which means we handled the exception and the request pipeline can stop here.

To register the exception handler, all you need to do is invoke the AddExceptionHandler() method in your Program.cs file.

builder.Services.AddExceptionHandler<CustomExceptionHandler>();

Run the application once again and this time you will see the problem details response similar to the following.

{
  "type": "Exception",
  "title": "Internal Server Error",
  "status": 500,
  "detail": "Product Not Found.",
  "instance": "/products"
}

Customizing Problem Details Response in ASP.NET Core

You can use the custom IExceptionHandler we implemented above to customize the generated Problem Details responses. For example, if you want to add extra details such as requestId or traceId in the problem details response you can add this information in the Extensions dictionary property available in the ProblemDetails object as follows.

var problemDetails = new ProblemDetails
{
    Title = statusCode == StatusCodes.Status500InternalServerError
        ? "Internal Server Error"
        : "A handled exception occurred",
    Status = statusCode,
    Type = exception?.GetType().Name,
    Detail = exception?.Message,
    Instance = httpContext.Request.Path
};

problemDetails.Extensions.Add("requestId", httpContext.TraceIdentifier);
            
Activity? activity = httpContext.Features.Get<IHttpActivityFeature>()?.Activity;
problemDetails.Extensions.TryAdd("traceId", activity?.Id);

Running the application again will show you problem details response similar to the following output.

{
  "type": "Exception",
  "title": "Internal Server Error",
  "status": 500,
  "detail": "Product Not Found.",
  "instance": "/products",
  "requestId": "0HN8TUOBKNMS1:00000001",
  "traceId": "00-d89d66409431f7af8aa9a7d994a3d5e9-8849ae6e668967dc-00"
}

If you are not using a global exception handler then the problem details can be customized in the Program.cs file using the CustomizeProblemDetails property available in the ProblemDetailsOptions class. These customizations will be applied to all auto-generated problem details.

builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Instance =
            $"{context.HttpContext.Request.Method} {context.HttpContext.Request.Path}";

        context.ProblemDetails.Extensions.TryAdd("requestId", context.HttpContext.TraceIdentifier);

        Activity? activity = context.HttpContext.Features.Get<IHttpActivityFeature>()?.Activity;
        context.ProblemDetails.Extensions.TryAdd("traceId", activity?.Id);
    };
});

The following output will be generated with the above code changes.

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
  "title": "An error occurred while processing your request.",
  "status": 500,
  "instance": "GET /products",
  "traceId": "00-85fc42f1c7f131ada01dd3a862fbc0c6-5abd388fe39477e7-00",
  "requestId": "0HN8TUSUDTVN2:00000001"
}

Returning Problem Details from Web APIs

If your Web API endpoint needs to provide additional error details to the clients in a way that complies with modern REST API error-handling best practices, then you can construct the ProblemDetails object with appropriate information and pass it to ASP.NET Core built-in helper methods such as NotFound or BadRequest.

public IActionResult Get()
{
    return NotFound(new ProblemDetails
    {
        Title = "Product Not Found",
        Status = StatusCodes.Status404NotFound,
        Detail = "The requested product could not be found."
    });
}

Calling the above endpoint will generate the following output in the browser.

{
  "title": "Product Not Found",
  "status": 404,
  "detail": "The requested product could not be found."
}

ASP.NET Core also has a built-in helper method called Problem that can be used to wrap error information into a ProblemDetails object and return it as the response. The content type of the response will be set to "application/problem+json" , which is the standard media type for Problem Details responses.

public IActionResult Get()
{
    return Problem(
        type: "Bad Request",
        title: "Invalid request",
        detail: "The provided request parameters are invalid.",
        statusCode: StatusCodes.Status400BadRequest);
}

Calling the above endpoint will generate the following output in the browser.

{
  "type": "Bad Request",
  "title": "Invalid request",
  "status": 400,
  "detail": "The provided request parameters are invalid.",
  "instance": "GET /products",
  "traceId": "00-0cdea3af3aa333ad4748f2b4d39823cb-821cfbe165d141c6-00",
  "requestId": "0HN92JDDE6O87:00000001"
}

Conclusion

Problem Details serve as an important mechanism for effective API error communication. By addressing the common anti-patterns that have long plagued HTTP APIs, the standard paves the way for a more reliable, secure, and user-friendly API ecosystem. As we move forward, the importance of adopting such standardized practices will only continue to grow, especially in our increasingly interconnected world.

READ ALSO:  Implementing Cookies Authentication in ASP.NET Core

Leave a Reply