Background and Motivation
API Controllers have a mechanism to auto generated Problem Details (https://datatracker.ietf.org/doc/html/rfc7807) for API Controller ActionResult. The mechanism is enabled by default for all API Controllers, however, it will only generates a Problem Details payload when the API Controller Action is processed and produces a HTTP Status Code 400+ and no Response Body, that means, scenarios like - unhandled exceptions, routing issues - won't produce a Problem Details payload.
Here is overview of when the mechanism will produce the payload:
❌ = Not generated
✅ = Automatically generated
Routing issues: ❌
Unhandled Exceptions: ❌
MVC
StatusCodeResult 400 and up: ✅ (based on SuppressMapClientErrors)
BadRequestResult and UnprocessableEntityResult are StatusCodeResult
- ObjectResult: ❌ (Unless a
ProblemDetails is specified in the input)
- eg.: BadRequestObjectResult and
UnprocessableEntityObjectResult
415 UnsupportedMediaType: ✅ (Unless when a ConsumesAttribute is defined)
406 NotAcceptable: ❌ (when happens in the output formatter)
Minimal APIs won't generate a Problem Details payload as well.
Here are some examples of reported issues by the community:
Proposed API
🎯 The goal of the proposal is to have the ProblemDetails generated, for all Status 400+ (except in Minimal APIs - for now) but the user need to opt-in and also have a mechanism that allows devs or library authors (eg. API Versioning) generate ProblemDetails responses when opted-in by the users.
An important part of the proposed design is the auto generation will happen only when a Body content is not provided, even when the content is a ProblemDetails that means scenario, similar to the sample below, will continue generate the ProblemDetails specified by the user and will not use any of the options to suppress the generation:
public ActionResult GetBadRequestOfT() => BadRequest(new ProblemDetails());
Overview:
- Minimal APIs will not have an option to autogenerate
ProblemDetails.
Exception Handler Middleware will autogenerate ProblemDetails only when no ExceptionHandler or ExceptionHandlerPath is provided, the IProblemDetailsService is registered,ProblemDetailsOptions.AllowedProblemTypes contains Server and a IProblemMetadata is added to the current endpoint.
Developer Exception Page Middleware will autogenerate ProblemDetails only when detected that the client does not accept text/html, the IProblemDetailsService is registered,ProblemDetailsOptions.AllowedProblemTypes contains Server and a IProblemMetadata is added to the current endpoint.
Status Code Pages Middleware default handler will generate a ProblemDetails only when detected the IProblemDetailsService is registered and the ProblemType requested is allowed and a IProblemMetadata is added to the current endpoint.
- A call to
AddProblemDetails is required and will register the IProblemDetailsService and a DefaultProblemDetailsWriter.
- A call to
AddProblemDetails is required and will register the IProblemDetailsService and a DefaultProblemDetailsWriter.
- When
APIBehaviorOptions.SuppressMapClientErrors is false, a IProblemMetadata will be added to all API Controller Actions.
- MVC will have an implementation of
IProblemDetailsWriter that allows content-negotiation that will be used for API Controllers, routing and exceptions. The payload will be generated only when a APIBehaviorMetadata is included in the endpoint.
- Addition
Problem Details configuration, using ProblemDetailsOptions.ConfigureDetails, will be applied for all autogenerated payload, including BadRequest responses caused by validation issues.
- MVC
406 NotAcceptable response (auto generated) will only autogenerate the payload when ProblemDetailsOptions.AllowedMapping contains Routing.
- Routing issues (
404, 405, 415) will only autogenerate the payload when ProblemDetailsOptions.AllowedMapping contains Routing.
A detailed spec is here.
namespace Microsoft.Extensions.DependencyInjection;
+public static class ProblemDetailsServiceCollectionExtensions
+{
+ public static IServiceCollection AddProblemDetails(this IServiceCollection services) { }
+ public static IServiceCollection AddProblemDetails(this IServiceCollection services, Action<ProblemDetailsOptions> configureOptions) +{}
+}
namespace Microsoft.AspNetCore.Http;
+public class ProblemDetailsOptions
+{
+ public ProblemTypes AllowedProblemTypes { get; set; } = ProblemTypes.All;
+ public Action<HttpContext, ProblemDetails>? ConfigureDetails { get; set; }
+}
+[Flags]
+public enum ProblemTypes: uint
+{
+ Unspecified = 0,
+ Server = 1,
+ Routing = 2,
+ Client = 4,
+ All = RoutingFailures | Exceptions | ClientErrors,
+}
+public interface IProblemDetailsWriter
+{
+ bool CanWrite(HttpContext context);
+ Task WriteAsync(HttpContext context, int? statusCode, string? title, string? type, string? detail, string? instance, IDictionary<string, object?>? extensions);
+}
+public interface IProblemDetailsService
+{
+ bool IsEnabled(ProblemTypes type);
+ Task WriteAsync(HttpContext context, EndpointMetadataCollection? currentMetadata = null, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null, IDictionary<string, object?>? extensions = null);
+}
namespace Microsoft.AspNetCore.Http.Metadata;
+public interface IProblemMetadata
+{
+ public int? StatusCode { get; }
+ public ProblemTypes ProblemType { get; }
+}
namespace Microsoft.AspNetCore.Diagnostics;
public class ExceptionHandlerMiddleware
{
public ExceptionHandlerMiddleware(
RequestDelegate next,
ILoggerFactory loggerFactory,
IOptions<ExceptionHandlerOptions> options,
DiagnosticListener diagnosticListener,
+ IProblemDetailsService? problemDetailsService = null)
{}
}
public class DeveloperExceptionPageMiddleware
{
public DeveloperExceptionPageMiddleware(
RequestDelegate next,
IOptions<DeveloperExceptionPageOptions> options,
ILoggerFactory loggerFactory,
IWebHostEnvironment hostingEnvironment,
DiagnosticSource diagnosticSource,
IEnumerable<IDeveloperPageExceptionFilter> filters,
+ IProblemDetailsService? problemDetailsService = null)
{}
}
Usage Examples
AddProblemDetails
Default options
var builder = WebApplication.CreateBuilder(args);
// Add services to the containers
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
var app = builder.Build();
// When problemdetails is enabled this overload will work even
// when the ExceptionPath or ExceptionHadler are not configured
app.UseExceptionHandler();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//Generate PD for 400+
app.UseStatusCodePages();
app.MapControllers();
app.Run();
Custom Options
var builder = WebApplication.CreateBuilder(args);
// Add services to the containers
builder.Services.AddControllers();
builder.Services.AddProblemDetails(options => {
options.AllowedProblemTypes = ProblemTypes.Server | ProblemTypes.Client | ProblemTypes.Routing;
options.ConfigureDetails = (context, problemdetails) =>
{
problemdetails.Extensions.Add("my-extension", new { Property = "value" });
};
});
var app = builder.Build();
// When Problem Details is enabled this overload will work even
// when the ExceptionPath or ExceptionHadler are not configured
app.UseExceptionHandler();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.MapControllers();
app.Run();
Creating a custom ProblemDetails writer
public class CustomWriter : IProblemDetailsWriter
{
public bool CanWrite(HttpContext context)
=> context.Response.StatusCode == 400;
public Task WriteAsync(HttpContext context, int? statusCode, string? title, string? type, string? detail, string? instance, IDictionary<string, object?>? extensions)
=> context.Response.WriteAsJsonAsync(CreateProblemDetails(statusCode, title, type, detail, instance, extensions));
private object CreateProblemDetails(int? statusCode, string? title, string? type, string? detail, string? instance, IDictionary<string, object?>? extensions)
{
throw new NotImplementedException();
}
}
// the new write need to be registered
builder.Services.AddSingleton<IProblemDetailsWriter, CustomWriter>();
Writing a Problem Details response with IProblemDetailsService
public Task WriteProblemDetails(HttpContext httpContext)
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
if (context.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService)
{
return problemDetailsService.WriteAsync(context);
}
return Task.CompletedTask;
}
Background and Motivation
API Controllers have a mechanism to auto generated
Problem Details(https://datatracker.ietf.org/doc/html/rfc7807) for API ControllerActionResult. The mechanism is enabled by default for allAPI Controllers, however, it will only generates aProblem Detailspayload when theAPI Controller Actionis processed and produces aHTTP Status Code 400+and noResponse Body, that means, scenarios like - unhandled exceptions, routing issues - won't produce aProblem Detailspayload.Here is overview of when the mechanism will produce the payload:
❌ = Not generated
✅ = Automatically generated
Routing issues: ❌
Unhandled Exceptions: ❌
MVC
StatusCodeResult400 and up: ✅ (based onSuppressMapClientErrors)BadRequestResultandUnprocessableEntityResultareStatusCodeResultProblemDetailsis specified in the input)UnprocessableEntityObjectResult415 UnsupportedMediaType: ✅ (Unless when aConsumesAttributeis defined)406 NotAcceptable: ❌ (when happens in the output formatter)Minimal APIswon't generate aProblem Detailspayload as well.Here are some examples of reported issues by the community:
Proposed API
🎯 The goal of the proposal is to have the
ProblemDetailsgenerated, for all Status 400+ (except inMinimal APIs- for now) but the user need to opt-in and also have a mechanism that allows devs or library authors (eg. API Versioning) generateProblemDetailsresponses when opted-in by the users.An important part of the proposed design is the auto generation will happen only when a Body content is not provided, even when the content is a
ProblemDetailsthat means scenario, similar to the sample below, will continue generate the ProblemDetails specified by the user and will not use any of the options to suppress the generation:Overview:
ProblemDetails.Exception Handler Middlewarewill autogenerateProblemDetailsonly when noExceptionHandlerorExceptionHandlerPathis provided, theIProblemDetailsServiceis registered,ProblemDetailsOptions.AllowedProblemTypescontainsServerand aIProblemMetadatais added to the current endpoint.Developer Exception Page Middlewarewill autogenerateProblemDetailsonly when detected that the client does not accepttext/html, theIProblemDetailsServiceis registered,ProblemDetailsOptions.AllowedProblemTypescontainsServerand aIProblemMetadatais added to the current endpoint.Status Code Pages Middlewaredefault handler will generate aProblemDetailsonly when detected theIProblemDetailsServiceis registered and theProblemTyperequested is allowed and aIProblemMetadatais added to the current endpoint.AddProblemDetailsis required and will register theIProblemDetailsServiceand aDefaultProblemDetailsWriter.AddProblemDetailsis required and will register theIProblemDetailsServiceand aDefaultProblemDetailsWriter.APIBehaviorOptions.SuppressMapClientErrorsisfalse, aIProblemMetadatawill be added to all API Controller Actions.IProblemDetailsWriterthat allows content-negotiation that will be used forAPI Controllers, routing and exceptions. The payload will be generated only when aAPIBehaviorMetadatais included in the endpoint.Problem Detailsconfiguration, usingProblemDetailsOptions.ConfigureDetails, will be applied for all autogenerated payload, includingBadRequestresponses caused by validation issues.406 NotAcceptableresponse (auto generated) will only autogenerate the payload whenProblemDetailsOptions.AllowedMappingcontainsRouting.404,405,415) will only autogenerate the payload whenProblemDetailsOptions.AllowedMappingcontainsRouting.A detailed spec is here.
namespace Microsoft.AspNetCore.Diagnostics; public class ExceptionHandlerMiddleware { public ExceptionHandlerMiddleware( RequestDelegate next, ILoggerFactory loggerFactory, IOptions<ExceptionHandlerOptions> options, DiagnosticListener diagnosticListener, + IProblemDetailsService? problemDetailsService = null) {} } public class DeveloperExceptionPageMiddleware { public DeveloperExceptionPageMiddleware( RequestDelegate next, IOptions<DeveloperExceptionPageOptions> options, ILoggerFactory loggerFactory, IWebHostEnvironment hostingEnvironment, DiagnosticSource diagnosticSource, IEnumerable<IDeveloperPageExceptionFilter> filters, + IProblemDetailsService? problemDetailsService = null) {} }Usage Examples
AddProblemDetails
Default options
Custom Options
Creating a custom
ProblemDetailswriterWriting a
Problem Detailsresponse withIProblemDetailsService