Skip to content

Allow consistent Problem Details generation #42212

@brunolins16

Description

@brunolins16

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;
} 

Metadata

Metadata

Assignees

Labels

DocsThis issue tracks updating documentationapi-approvedAPI was approved in API review, it can be implementedblog-candidateConsider mentioning this in the release blog postold-area-web-frameworks-do-not-use*DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions