The Problem Details for HTTP APIs RFC provides a unified, machine-readable and standardized recipe for exposing error information out of your HTTP APIs - which is of course beneficial both for the API authors, as well as the integrating parties.
ASP.NET Core has supported problem details since version 2.1, however it was not been uniformly used across all ASP.NET Core components. It was possible to return the Problem Details response manually, or the framework could generate it automatically in several specific cases. Even the official documentation referred to a third-party middleware in order to get a better Problem Details experience.
This is finally changing in .NET 7.
Out of the box support since .NET 7 Preview 7 π
Thanks to the great work of Bruno Oliveira, ASP.NET Core on .NET 7 is now getting a much better support for Problem Details. Going forward, assuming everything is configured properly, most of the non-success responses and exceptions will be possible to be automatically converted to and exposed to the callers in the Problem Details format.
The feature first appeared in .NET 7 Preview 7, which came out on 9 August 2022. Since this is an official public preview, it can be easily installed and explored.
Configuration π
The feature is opt-in - to not creating a breaking change - and the primary configuration mechanism is to add the following call when bootstrapping the services collection of ASP.NET Core:
services.AddProblemDetails();
Once that is in place, there are three primary ways to get Problem Details to become part of your application, each one related to a slightly different aspect of the framework.
First, by adding the default error handler, all unhandled exceptions will be given the Problem Details treatment:
app.UseExceptionHandler();
With this in place, any unhandled exception from e.g. an MVC controller, a filter, a minimal API endpoint or a middleware, would get converted to the following response:
500 Internal Server Error
Content-Type: application/problem+json; charset=utf-8
Cache-Control: no-cache,no-store
Pragma: no-cache
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title": "An error occurred while processing your request.",
"status": 500,
"traceId": "00-0aa7d64ad154a1e1853c413a0def982d-195d3558c90f7876-00"
}
Secondly, enabling the status code pages middleware, will allow Problem Details to be used in some extra non-exception related framework scenarios, such as a 404 occurring due to a non-existent route or a 405 occurring due to a caller using an invalid HTTP method on an existing endpoint. This is enabled via:
app.UseStatusCodePages();
And then a sample response would be (in this particular case, when calling a POST on a GET endpoint):
405 Method Not Allowed
Content-Type: application/problem+json; charset=utf-8
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.5",
"title": "Method Not Allowed",
"status": 405
}
Finally, Problem Details can also be used in conjunction with the developer exception page middleware. In that case, a much richer set of information, including the stack trace and the request details, will also be added to the Problem Details response. Because of that, this should obviously be avoided in production environment.
app.UseDeveloperExceptionPage();
The exception response would then look similar to:
500 Internal Server Error
Content-Type: application/problem+json; charset=utf-8
Cache-Control: no-cache,no-store
Pragma: no-cache
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title": "System.Exception",
"status": 500,
"detail": "controller error!",
"traceId": "00-e5912bbe6bd895ee1d7c5e8df3d5b1fa-168fab2c8219a7c5-00",
"exception": {
"details": [
{
"message": "controller error!",
"type": "System.Exception",
"stackFrames": [
{
"function": "net70_webapi.Controllers.WeatherForecastController.Get()",
"file": "/Users/demo/dev/net70-webapi/Controllers/WeatherForecastController.cs",
"line": 24,
"preContextLine": 18,
"preContextCode": [
" _logger = logger;",
" }",
"",
" [HttpGet(Name = \"GetWeatherForecast\")]",
" public IEnumerable<WeatherForecast> Get()",
" {"
],
"contextCode": [
" throw new Exception(\"controller error!\");"
],
"postContextCode": [
" return Enumerable.Range(1, 5).Select(index => new WeatherForecast",
" {",
" Date = DateTime.Now.AddDays(index),",
" TemperatureC = Random.Shared.Next(-20, 55),",
" Summary = Summaries[Random.Shared.Next(Summaries.Length)]",
" })"
],
"errorDetails": null
}
]
}
],
"headers": {
"Accept": [
"*/*"
],
"Connection": [
"keep-alive"
],
"Host": [
"localhost:7281"
],
"User-Agent": [
"PostmanRuntime/7.29.2"
],
"Accept-Encoding": [
"gzip, deflate, br"
]
},
"error": "System.Exception: controller error!\n at net70_webapi.Controllers.WeatherForecastController.Get() in /Users/demo/dev/net70-webapi/Controllers/WeatherForecastController.cs:line 24\n at lambda_method1(Closure, Object, Object[])\n at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()\n--- End of stack trace from previous location ---\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()\n--- End of stack trace from previous location ---\n at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)\n at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)\n at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)\n at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)\n at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)\n at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)\n at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)",
"path": {
"value": "/weatherforecast",
"hasValue": true
},
"endpoint": "net70_webapi.Controllers.WeatherForecastController.Get (net70-webapi)",
"routeValues": {
"action": "Get",
"controller": "WeatherForecast"
}
}
}
Further customizations π
The default behaviors can be further customized in two ways. First of all, the registration method AddProblemDetails() provides an overload that allows configuring some basic options against ProblemDetailsOptions. The typical use case would be to register own delegate which would be invoked when an instance of ProblemDetails model gets created.
The example below shows injecting a custom identifier to the Instance field. Assuming that you would log the identifier into your logging system, it could then be used to easily located the error in the logs - when the caller shares that identifier.
builder.Services.AddProblemDetails(o => o.CustomizeProblemDetails = ctx =>
{
var problemCorrelationId = Guid.NewGuid().ToString();
//log problemCorrelationId into logging system
ctx.ProblemDetails.Instance = problemCorrelationId;
});
More sophisticated customizations are also possible. The call to AddProblemDetails() actually internally registers the defaults implementations of a new interface IProblemDetailsService, which is responsible for the generation of the Problem Details model, as well as an implementation of IProblemDetailsWriter, which is then responsible for writing the model to the HTTP response stream.
Both of them can be replaced with own custom variants, registered in the DI container - in which case the call to AddProblemDetails() would not longer be needed. At that point we have full control of the creation and writing of the Problem Details response.