-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Enable Option to Use Content Negotiation for ProblemDetails #45159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
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
We'll hold off on approving this until we're confident what we want the default to be. Ideally, we'll have whatever we name the option false by default. |
Great questions.
This should be useful in any scenario where I was specifically thinking in public DefaultProblemDetailsWriter(IOptions<ProblemDetailsOptions> options)
{
_options = options.Value;
}
public bool CanWrite(ProblemDetailsContext context)
{
+ if (_options.SkipContentNegotiation)
+ {
+ // always return true because we aren't honoring Accept
+ return true;
+ }
...
} However, this could just as easily be used for the
were a few other variants I considered. Those can be used to default to I did consider Consider the following request: POST /order HTTP/3
Host: localhost
Content-Type: application/json
Content-Length: 420
{"customer": "John Doe", "product": "Contoso soap", "quantity": 42} If this results in a client error, say validation, and
I'm open to other names, but that's my 2¢.
Arguably, the behavior is already broken in .NET 7; at least, using the As you point out, this option could be used in other places and could be tricky with the current, expected behaviors. Another alternative could be to use: public bool? SkipContentNegotiation { get; set; } This would be expected to have the following behaviors:
An enumeration might be more intention revealing here: public enum ProblemDetailsContentNegotiation
{
Default = 0, // whatever the existing, default behavior is; backward compatible
Strict = 1, // could also be Required, Enforce
Skip = 2, // could also be None, Optional
}
public ProblemDetailsContentNegotiation ContentNegotiation { get; set; } |
I have been thinking about this proposal and one thing that, since the initial proposal, annoyed me is that we are adding an option to Let me try explaining what I mean about "will not be always honored". We are adding the new flag I was thinking about naming it something like Eg.: // Try to write using all registered writers (not including the defaullt writer)
// sequentially and stop at the first one that
// `canWrite.
for (var i = 0; i < _writers.Length; i++)
{
var selectedWriter = _writers[i];
if (selectedWriter.CanWrite(context))
{
return selectedWriter.WriteAsync(context);
}
}
if (_options.AlwaysFallbackToDefaultWriter || _defaultWriter.CanWrite(context))
{
return _defaultWriter.WriteAsync(context);
} @commonsensesoftware Thoughts? |
@brunolins16 ignoring options is always a possibility. Extenders don't have to honor the I think what we are collectively saying and agreeing to, in somewhat different ways, is that content negotiation validation in conjunction with the proposed option needs to be lifted out of the internal space. There are two ways I can currently think of that this could be done. Option 1Refactor the validation logic for each type of media type into an interface that would have a concrete implementation for 2 default rules:
The While this would work, I concede that it's probably over-engineered, which is why I haven't bothered showcasing what the code might look like (but I can 😉). These are likely the only two content negotiation scenarios that will ever exist. Option 2Create a new base class that has the common logic baked into. Something like: public abstract class ProblemDetailsWriterBase : IProblemDetailsWriter
{
private static readonly MediaTypeHeaderValue _jsonMediaType = new("application/json");
private static readonly MediaTypeHeaderValue _problemDetailsJsonMediaType = new("application/problem+json");
private readonly IOptions<ProblemDetailsOptions> _options;
protected ProblemDetailsWriterBase(IOptions<ProblemDetailsOptions> options) => _options = options;
protected ProblemDetailsOptions Options => _options.Value;
bool IProblemDetailsWriter.CanWrite(ProblemDetailsContext context) =>
CanNegotiateContent(context) && CanWrite(context);
public abstract ValueTask WriteAsync(ProblemDetailsContext context);
protected virtual bool CanWrite(ProblemDetailsContext context) => true;
protected virtual bool CanNegotiateContent(ProblemDetailsContext context)
{
if (Options.SkipContentNegotiation)
{
return true;
}
var headers = context.HttpContext.Request.Headers;
var accept = headers.Accept.GetList<MediaTypeHeaderValue>();
if (IsAcceptable(accept))
{
return true;
}
// if Accept isn't specified, infer from Content-Type if possible
if (accept.Count == 0 &&
headers.Get<MediaTypeHeaderValue>(HeaderNames.ContentType) is MediaTypeHeaderValue contentType)
{
return IsSupportedMediaType(contentType);
}
return false;
}
protected bool IsAcceptable(IReadOnlyList<MediaTypeHeaderValue> accept)
{
// Based on https://www.rfc-editor.org/rfc/rfc7231#section-5.3.2 a request
// without the Accept header implies that the user agent
// will accept any media type in response
if (accept.Count == 0)
{
return true;
}
for (var i = 0; i < accept.Count; i++)
{
var value = accept[i];
if (_jsonMediaType.IsSubsetOf(value) ||
_problemDetailsJsonMediaType.IsSubsetOf(value))
{
return true;
}
}
return false;
}
protected bool IsSupportedMediaType(MediaTypeHeaderValue contentType) =>
jsonMediaType.IsSubsetOf( contentType )
|| problemDetailsJsonMediaType.IsSubsetOf( contentType );
} Using this approach, all of the logic in |
@commonsensesoftware appreciate your thoughts and option 2 is not bad but I really believe we could try simplifying instead over engineering it. We can have the |
@brunolins16 I agree - no need to over-engineer things. Rolling up things into the Just as things can be over-engineered, they can also be over-configured. Having some kind of delegate is unnecessary IMO. I suspect most people will not change whatever the default setting settles on. For those that do change it, they will likely only want all validation or none - as originally described. Anyone that needs or wants to configure things further should be expected to understand the 🐰 🕳️ they are going down. This is where keeping it simple and enforcing specific behaviors are at odds. Expecting a developer to know and honor the option when they down this path is not unreasonable to me. Similarly extending the out-of-the-box functionality should be possible without having to reimplement everything (as is currently the case). Ultimately, that's why the original proposal is a single, simple option setting that you can configure. Honoring the setting should be the onus of the developers that extend the functionality as is the case with every other setting. |
Thanks for contacting us. We're moving this issue to the |
Looks like the next step here is to revise the proposed API and decide how to proceed. Moving this back to .NET 8 planning; the team is fairly slammed at the moment so we don't expect to have cycles for this in the near term. |
@adityamandaleeka The primary work is settling on the the API design, which seems largely to be naming. The effort to execute is trivial IMHO and I'm happy to put the PR for it once some decisions are locked. If there is something more I can do to help move things along, just let me know. |
We're experiencing an issue that might be related to this and also #45051. We've enabled versioning on our Minimal API using the However, since this is a more specific media type than I guess it might be necessary to permit more specific media types by allowing for this in the conditional checks, for example: if (_jsonMediaType.IsSubsetOf(acceptHeaderValue) ||
acceptHeaderValue.IsSubsetOf(_jsonMediaType) ||
_problemDetailsJsonMediaType.IsSubsetOf(acceptHeaderValue) ||
acceptHeaderValue.IsSubsetOf(_problemDetailsJsonMediaType))
{
return true;
} This is unfortunately blocking us from using both Minimal API versioning with a media type header in conjunction with problem details responses, so we'll have to fall back to another version reader implementation until this is addressed. |
@source-studio I have confirmed that this is, in fact, a 🐞 . The current adapter logic is backward. It should be: if ( acceptHeaderValue.IsSubsetOf( jsonMediaType ) ||
acceptHeaderValue.IsSubsetOf( problemDetailsJsonMediaType ) )
{
return true;
} The naming is a feels off IMHO. Please file a new bug in the API Versioning repo. I'll have it patched for you ASAP. |
Background and Motivation
The implementation of
DefaultProblemDetailsWriter
strictly enforces thatProblemDetails
are only written ifAccept
is present and can be content negotiated. This is contrary to the HTTP specification. RFC 7231 §5.3.2 semantics for theAccept
header states:The current behavior is very sensible as a default. It requires a client to understand they can get a
ProblemDetails
response; however, it is very common for API authors to not honor content negotiation for errors.DefaultProblemDetailsWriter
isinternal
andsealed
with functionality that cannot be easily extended or customized. Ultimately, an API author simply wants to decide if content negotiation should take place forProblemDetails
and that shouldn't require customization when trivial configuration will suffice.Proposed API
The proposed API would be to extend
ProblemDetailsOptions
to include a property to determine whether content negotiation should be used. The default value will betrue
, which retains the current behavior. If a developer sets the value tofalse
, the expectation is that content negotiation is skipped.Usage Examples
The default configuration where content negotiation is used. This is the same behavior as today.
A new configuration option which can instruct
IProblemDetailsWriter
implementations not to honor content negotiation.Risks
Nothing apparent. The default configuration and behavior would be retained.
Additional Information
In accordance with RFC 7231 §5.3.2, if
Accept
is unspecified or is empty, then the value ofProblemDetailsOptions.UseContentNegotiation
should be ignored and implied to befalse
. This behavior is being tracked in #45051.The text was updated successfully, but these errors were encountered: