Closed brunolins16 closed 2 years ago
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
Problem Details Middleware does a lot (if not all?) of this already and seems a lot less complex than the proposal here. Perhaps the work there could be piggybacked on some how? It definitely would be nice to not need a reference to a 3rd party to get some of what that library offers
@pinkfloydx33 Thanks for the feedback. I'd like to know why do you think this proposal is complex? The usage will be just a call for service.AddProblemDetails
that will enable the consistent behavior.
Also, just to let you know I don't want to introduce a middleware because it doesn't compose with the other middlewares. As example, in my proposal, the ExceptionMiddleware will behave differently when the service is available instead of have another middleware responsible for do the same.
I agree, this doesn't look complicated (though I haven't dug in deeply as yet).
cc @khellang for feedback.
Woah, finally 😅 This will most certainly kill my middleware (which I don't necessarily mind) if done right.
I see the current proposal doesn't mention the existing ProblemDetailsFactory
extensibility point - is that on purpose?
What about a quick way to configure mapping of exceptions to different types of problem details? Is that something you've considered?
I'm on the phone right now, but I'll take a deeper look and see if I can come up with some additional feedback later 👌
Sorry, "complex" may have been the wrong word. The middleware I cite makes use of ProblemDetailsFactory
and doesn't introduce much in the way of anything new (other than some incredible customization options), so I just wonder if the new interfaces/writer are necessary?
I'm using the middleware both in my APIs and in my YARP-backed API gateway (to catch anything that may slip through and to augment 50x statuses), where I've also got Custom StatusCode pages / error pages and have wired everything together to get a nice problem details response in 100% of cases. That said, I did have to reinvent content negotiation in the custom-status-code-page to ensure my gateway returns a nice html page when expected and a problem response otherwise. So that part of the proposal appeals to me since I'd likely be able to remove the hack.
If the goal is to not have this as a middleware, I guess I understand the need for the extra interfaces. It just seems weird this can't be a couple extra options and some tweaks to ProblemDetailsFactory
/[Validation]ProblemDetails
. I'm no expert on ASP internals though, so take that for what it's worth.
No matter the solution, I welcome this. I am a very satisfied user of @khellang's project and have it included by default in our internal templates. It would definitely be nice to have something like it in-box
(Thinking on it a bit more, I suppose without a middleware you need some mechanism to convert general error responses into problem details, somewhere higher up in the stack where the writer can be injected/available and likely optimized... it's making a bit more sense ).
I am not sure if you have a chance to see the detailed design but problemdetailsfactory will be use to handle APi controllers as well but since the idea is to create something that does not rely on MVC the default writer
is simpler and does not supports content-negotiation.
That said, those are all good feedback and will make me review the proposal this week. Thanks.
some mechanism to convert general error responses into problem details
Yeah, my understanding is that this is kinda "repurposing" the error handler middleware to produce ProblemDetails
without much customization. My middleware is duplicating a lot of what the error handler middleware is already doing.
I still think the proposal needs a few more knobs before you'll be able to make the switch and keep the same functionality. Configuring different mapping functions for different exception types is one of them.
I still think the proposal needs a few more knobs before you'll be able to make the switch and keep the same functionality. Configuring different mapping functions for different exception types is one of them.
@khellang the ProblemDetailsOptions.ConfigureDetails
is a general-purpose extension point but I agree that more knobs are needed, for exceptions, but I think those should not go to the central configuration and instead each part of the framework should provide their extensibility. A more complex extension point is the IProblemDetailsWriter
but I don't see a lot applications implementing this but libraries could potentially do, it is not as complete as implementing a middleware like yours but give more flexibility about how to handle a problem details.
In my initial proposal I included the ExceptionHandlerOptions.ConfigureDetails
, that will basically allow the ExceptionHandler Middleware compose with the general-purpose problem details:
public class ExceptionHandlerOptions
{
+ public Action<IExceptionHandlerFeature, ProblemDetails>? ConfigureDetails { get; set; }
}
I prefer not include it in my final proposal, and make it follow up issue, but I will talk about it during the API review. Also, it potentially could be similar to your middleware Map
options.
Since you have been working in your middleware for while I really appreciate your feedback.
Yeah, it makes sense to scope exception mapping to the error handler middleware, since it's the one actually handling the exceptions, but I also see it as a way to misconfigure stuff.
What if you try to set ConfigureDetails
on ExceptionHandlerOptions
without calling AddProblemDetails
? Or if AllowedMapping
doesn't include Exceptions
?
but I also see it as a way to misconfigure stuff. What if you try to set ConfigureDetails on ExceptionHandlerOptions without calling AddProblemDetails? Or if AllowedMapping > doesn't include Exceptions?
It will be a no-op, since the service is not registered (or if registered and not Exceptions allowed) the exception handler will never produce a ProblemDetails
. It is not clear and the exact reason why I prefer keeping out of my final proposal.
BTW, if you call services.UseExceptionHandler()
without configuring the ExceptionHandler
or ExceptionPath
today it will throw a misconfigured exception, and in my proposal it will be allowed if AddProblemDetails
was called.
API review notes:
ValueTask
for Task
returning methodsIProblemMetadata
and IProblemTypes
to IProblemDetailsMetadata
IProblemDetailsTypes
All = RoutingFailures | Exceptions | ClientErrors,
IProblemDetailsService.IsEnabled(ProblemTypes type);
needed?
@brunolins16 will update the proposal for either email or next review
Updated API Proposal:
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 ProblemDetailsTypes: uint
+{
+ Unspecified = 0,
+ Server = 1,
+ Routing = 2,
+ Client = 4,
+ All =Server | Routing | Client ,
+}
+public class ProblemDetailsContext
+{
+ public ProblemDetailsContext(HttpContext httpContext) {}
+ public HttpContext HttpContext { get; }
+ public EndpointMetadataCollection? AdditionalMetadata { get; init; }
+ public ProblemDetails ProblemDetails { get; init; } = new ProblemDetailt();
+}
+public interface IProblemDetailsWriter
+{
+ ValueTask<bool> WriteAsync(ProblemDetailsContext context);
+}
+public interface IProblemDetailsService
+{
+ ValueTask WriteAsync(ProblemDetailsContext context);
+}
namespace Microsoft.AspNetCore.Http.Metadata;
+public interface IProblemDetailsMetadata
+{
+ 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) {}
}
public class DeveloperExceptionPageMiddleware
{
+ public DeveloperExceptionPageMiddleware(
+ RequestDelegate next,
+ IOptions<DeveloperExceptionPageOptions> options,
+ ILoggerFactory loggerFactory,
+ IWebHostEnvironment hostingEnvironment,
+ DiagnosticSource diagnosticSource,
+ IEnumerable<IDeveloperPageExceptionFilter> filters,
+ IProblemDetailsService? problemDetailsService) {}
}
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
API Review Notes:
public enum ProblemDetailsTypes: uint
?
ConfigureDetails
to ConfigureProblemDetails
?
ProblemDetailsOptions.ConfigureDetails
-> ProblemDetailsOptions.CustomizeProblemDetails
and make the parameter ProblemDetailsContext
.ProblemDetailsKinds
instead of ProblemDetailsTypes
?
Server
, Routing
and Client
clear enough enum cases?
required init
for ProblemDetailsContext
?
IProblemDetailsMetadata
until we add something like ProblemDetailsTypes
?
ExceptionHandlerMiddleware
and DeveloperExceptionPageMiddleware
classes (we have to keep the public classes with the existing constructors ofc), but we can add the new constructors to the internal class which the extension methods will then use.API Approved!
namespace Microsoft.Extensions.DependencyInjection;
+public static class ProblemDetailsServiceCollectionExtensions
+{
+ public static IServiceCollection AddProblemDetails(this IServiceCollection services) { }
+ public static IServiceCollection AddProblemDetails(this IServiceCollection services, Action<ProblemDetailsOptions>? configure) { }
+}
namespace Microsoft.AspNetCore.Http;
+public class ProblemDetailsOptions
+{
+ public Action<ProblemDetailsContext>? CustomizeProblemDetails { get; set; }
+}
+public class ProblemDetailsContext
+{
+ public required HttpContext HttpContext { get; init; }
+ public EndpointMetadataCollection? AdditionalMetadata { get; init; }
+ public ProblemDetails ProblemDetails { get; init; } = new ProblemDetails();
+}
+public interface IProblemDetailsWriter
+{
+ ValueTask<bool> TryWriteAsync(ProblemDetailsContext context);
+}
+public interface IProblemDetailsService
+{
+ ValueTask WriteAsync(ProblemDetailsContext context);
+}
During the PR review was found that we could have a small memory allocation improvement (avoiding multiple async state machines) if we do the following API change comparing with the approved API:
public interface IProblemDetailsWriter
{
- ValueTask<bool> TryWriteAsync(ProblemDetailsContext context);
+ ValueTask WriteAsync(ProblemDetailsContext context);
+ bool CanWrite(ProblemDetailsContext context);
}
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
Quick API review notes:
API with update approved!
namespace Microsoft.Extensions.DependencyInjection;
+public static class ProblemDetailsServiceCollectionExtensions
+{
+ public static IServiceCollection AddProblemDetails(this IServiceCollection services) { }
+ public static IServiceCollection AddProblemDetails(this IServiceCollection services, Action<ProblemDetailsOptions>? configure) { }
+}
namespace Microsoft.AspNetCore.Http;
+public class ProblemDetailsOptions
+{
+ public Action<ProblemDetailsContext>? CustomizeProblemDetails { get; set; }
+}
+public class ProblemDetailsContext
+{
+ public required HttpContext HttpContext { get; init; }
+ public EndpointMetadataCollection? AdditionalMetadata { get; init; }
+ public ProblemDetails ProblemDetails { get; init; } = new ProblemDetails();
+}
+public interface IProblemDetailsWriter
+{
+ bool CanWrite(ProblemDetailsContext context);
+ ValueTask WriteAsync(ProblemDetailsContext context);
+}
+public interface IProblemDetailsService
+{
+ ValueTask WriteAsync(ProblemDetailsContext context);
+}
So what would be the recommended method for customizing the Problem Details based on caught exceptions? Assuming we're using the Exception handling middleware, would we just check for the IExceptionHandlerFeature
in the context.HttpContext.Features
from within the CustomizeProblemDetails
callback?
@pinkfloydx33 Exactly, and you can do this on the CustomizeProblemDetails
callback (simplest way) or create a Custom Writer that handle when the feature is available.
public class SamppleExceptionWriter : IProblemDetailsWriter
{
public bool CanWrite(ProblemDetailsContext context)
=> context.HttpContext.Response.StatusCode >= 500 &&
context.HttpContext.Features.Get<IExceptionHandlerFeature>() is not null;
public ValueTask WriteAsync(ProblemDetailsContext context)
{
var httpContext = context.HttpContext;
// Customize you PD (if you want you need to explicit call the CustomizeProblemDetails here)
// Write to the response
return new ValueTask(httpContext.Response.WriteAsJsonAsync(context.ProblemDetails));
}
}
Just to let you know, I am planning to create a new issue to propose changes, in .NET 8, to the ExceptionHandler middleware
to support something similar to what we have in @khellang middleware.
Cool, thanks for the example. That's pretty much what I was thinking. The trick with the writers however will be ensuring you add them in the correct order since it's the first CanWrite
that wins. Not a big deal of course
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 Details
payload when theAPI Controller Action
is processed and produces aHTTP Status Code 400+
and noResponse Body
, that means, scenarios like - unhandled exceptions, routing issues - won't produce aProblem 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 onSuppressMapClientErrors
)BadRequestResult
andUnprocessableEntityResult
areStatusCodeResult
ProblemDetails
is specified in the input)UnprocessableEntityObjectResult
415 UnsupportedMediaType
: ✅ (Unless when aConsumesAttribute
is defined)406 NotAcceptable
: ❌ (when happens in the output formatter)Minimal APIs
won't generate aProblem Details
payload as well.Here are some examples of reported issues by the community:
32957
16889
4953
Proposed API
🎯 The goal of the proposal is to have the
ProblemDetails
generated, 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) generateProblemDetails
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:Overview:
ProblemDetails
.Exception Handler Middleware
will autogenerateProblemDetails
only when noExceptionHandler
orExceptionHandlerPath
is provided, theIProblemDetailsService
is registered,ProblemDetailsOptions.AllowedProblemTypes
containsServer
and aIProblemMetadata
is added to the current endpoint.Developer Exception Page Middleware
will autogenerateProblemDetails
only when detected that the client does not accepttext/html
, theIProblemDetailsService
is registered,ProblemDetailsOptions.AllowedProblemTypes
containsServer
and aIProblemMetadata
is added to the current endpoint.Status Code Pages Middleware
default handler will generate aProblemDetails
only when detected theIProblemDetailsService
is registered and theProblemType
requested is allowed and aIProblemMetadata
is added to the current endpoint.AddProblemDetails
is required and will register theIProblemDetailsService
and aDefaultProblemDetailsWriter
.AddProblemDetails
is required and will register theIProblemDetailsService
and aDefaultProblemDetailsWriter
.APIBehaviorOptions.SuppressMapClientErrors
isfalse
, aIProblemMetadata
will be added to all API Controller Actions.IProblemDetailsWriter
that allows content-negotiation that will be used forAPI Controllers
, routing and exceptions. The payload will be generated only when aAPIBehaviorMetadata
is included in the endpoint.Problem Details
configuration, usingProblemDetailsOptions.ConfigureDetails
, will be applied for all autogenerated payload, includingBadRequest
responses caused by validation issues.406 NotAcceptable
response (auto generated) will only autogenerate the payload whenProblemDetailsOptions.AllowedMapping
containsRouting
.404
,405
,415
) will only autogenerate the payload whenProblemDetailsOptions.AllowedMapping
containsRouting
.A detailed spec is here.
Usage Examples
AddProblemDetails
Default options
Custom Options
Creating a custom
ProblemDetails
writerWriting a
Problem Details
response withIProblemDetailsService