Description
Is there an existing issue for this?
- I have searched the existing issues
Describe the bug
builder.Services.AddProblemDetails()
adds the internal type DefaultProblemDetailsWriter
. As seen here:
The CanWrite
implement returns false
when the Accept
header is missing or empty. This is violates conformance with RFC 7231 §5.3.2, which states:
A request without any Accept header field implies that the user agent will accept any media type in response.
This results in a behavioral breaking change for a server expected to return ProblemDetails
to a client.
Expected Behavior
DefaultProblemDetailsWriter.CanWrite
should return true
and ultimately write ProblemDetails
when Accept
is missing or empty.
Steps To Reproduce
var builder = WebApplication.CreateBuilder( args );
builder.Services
.AddProblemDetails()
.AddApiVersioning( options => options.ApiVersionReader = new MediaTypeApiVersionReader() );
var forecast = app.MapGroup( "/weatherforecast" ).WithApiVersionSet();
forecast.MapPost( "/", ( WeatherForecast forecast ) => Results.Ok() ).HasApiVersion( 1.0 );
app.Run();
POST /weatherforecast HTTP/2
Host: localhost:5000
Content-Type: application/json; v=2.0
Content-Length: 61
{"date": "2022-11-12", "temperatureC": 42, "summary": "Cold"}
The response is 415
(because version v=2.0
is not supported), but no ProblemDetails
are included.
Exceptions (if any)
No response
.NET Version
7.0.100
Anything else?
DefaultProblemDetailsWriter
is internal
and sealed
so it cannot be easily extended or changed. It also uses other internal types or properties, which makes it difficult or impossible to fork.
A possible workaround is to decorate the implementation, but this is a very yucky DI setup:
private static void TryAddProblemDetailsRfc7231Compliance( IServiceCollection services )
{
var descriptor = services.FirstOrDefault( IsDefaultProblemDetailsWriter );
if ( descriptor == null )
{
return;
}
var decoratedType = descriptor.ImplementationType!;
var lifetime = descriptor.Lifetime;
services.Add( Describe( decoratedType, decoratedType, lifetime ) );
services.Replace( Describe( typeof( IProblemDetailsWriter ), sp => NewProblemDetailsWriter( sp, decoratedType ), lifetime ) );
static bool IsDefaultProblemDetailsWriter( ServiceDescriptor serviceDescriptor ) =>
serviceDescriptor.ServiceType == typeof( IProblemDetailsWriter ) &&
serviceDescriptor.ImplementationType?.FullName == "Microsoft.AspNetCore.Http.DefaultProblemDetailsWriter";
static Rfc7231ProblemDetailsWriter NewProblemDetailsWriter( IServiceProvider serviceProvider, Type decoratedType ) =>
new( (IProblemDetailsWriter) serviceProvider.GetRequiredService( decoratedType ) );
}
Additional Consideration
RFC 7231 §5.3.2 semantics for the Accept
header also states:
...disregard the header field by treating the response as if it is not subject to content negotiation.
I propose that ProblemDetailsOptions
add a new property to allow that configuration. The default value would be true
and have parity with the current behavior. Developers can set it to false
to retain the ProblemDetailsFactory
behavior versus the new IProblemDetailsService
behavior.
public class ProblemDetailsOptions
{
/// <summary>
/// The operation that customizes the current <see cref="Mvc.ProblemDetails"/> instance.
/// </summary>
public Action<ProblemDetailsContext>? CustomizeProblemDetails { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether to use HTTP content negotiation.
+ /// </summary>
+ /// <value>True if the content negotiation is used; otherwise, false. The default value is <c>true</c>.</value>
+ public bool UseContentNegotiation { get; set; } = true;
}
I realize this is a feature request. I'm happy to create a separate issue for it, but since it's so closely related to this bug, and arguably part of it (because it can't be achieved), I'm starting with the proposal here.