Skip to content

DefaultProblemDetailsWriter Is Not RFC 7231 Compliant #45051

Closed
@commonsensesoftware

Description

@commonsensesoftware

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.

Metadata

Metadata

Assignees

Labels

old-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

Relationships

None yet

Development

No branches or pull requests

Issue actions