-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Description
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
StatusCodeResult400 and up: ✅ (based onSuppressMapClientErrors)BadRequestResultandUnprocessableEntityResultareStatusCodeResult
- ObjectResult: ❌ (Unless a
ProblemDetailsis specified in the input)- eg.: BadRequestObjectResult and
UnprocessableEntityObjectResult
- eg.: BadRequestObjectResult and
415 UnsupportedMediaType: ✅ (Unless when aConsumesAttributeis 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:
- Usage of ProblemDetails is inconsistent throughout ASP.NET Core #32957
- ReturnHttpNotAcceptable=true Does Not Return ProblemDetails #16889
- ProblemDetails is not returned for 404NotFound and 500Exception #4953
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 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.- A call to
AddProblemDetailsis required and will register theIProblemDetailsServiceand aDefaultProblemDetailsWriter. - A call to
AddProblemDetailsis required and will register theIProblemDetailsServiceand aDefaultProblemDetailsWriter. - When
APIBehaviorOptions.SuppressMapClientErrorsisfalse, aIProblemMetadatawill be added to all API Controller Actions. - MVC will have an implementation of
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. - Addition
Problem Detailsconfiguration, usingProblemDetailsOptions.ConfigureDetails, will be applied for all autogenerated payload, includingBadRequestresponses caused by validation issues. - MVC
406 NotAcceptableresponse (auto generated) will only autogenerate the payload whenProblemDetailsOptions.AllowedMappingcontainsRouting. - Routing issues (
404,405,415) will only autogenerate the payload whenProblemDetailsOptions.AllowedMappingcontainsRouting.
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;
}