-
Notifications
You must be signed in to change notification settings - Fork 2.1k
[Design] Add support for API error response type based on RFC 7807 #6695
Conversation
11e4166
to
0630ae5
Compare
|
||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer | ||
{ | ||
public interface IErrorPolicy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be an IApiDescriptorConvention
instead? There's nothing restrictive about it being for errors only here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unsure currently. I would rather see the O/M for problem fleshed out first rather than focus on the deets of how the conventions for errors work.
/// <summary> | ||
/// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807. | ||
/// </summary> | ||
public class Problem : Dictionary<string, object> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Support for Extension Members
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh, is there another way here besides extending dictionary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll play around with it. I just went with what we do in SerializableError
because it looked easy
public string Type | ||
{ | ||
get => GetValue<string>(nameof(Type)); | ||
set => Add(nameof(Type), value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By using Add
here, it'll throw if the Type
property has already been set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch!
StatusCode = statusCodeResult.StatusCode, | ||
}; | ||
} | ||
else if (statusCode == StatusCodes.Status400BadRequest) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already checked above and will never be true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't blame @pranavkm - blame my bad prototype code 😆
I ❤️ this! I noticed @rynowak's KodKod repo a few days ago and saw this coming. I use something like this (problem+json) in all my APIs. I can't help thinking this would be even better as middleware, so you could add it to the start of the pipeline and make sure all non-200 responses would be returned in a proper representation. It would also be nice to be able to map different exceptions to different problem types. Something like |
@@ -50,6 +50,17 @@ public ActionResult(ActionResult result) | |||
return new ActionResult<TValue>(result); | |||
} | |||
|
|||
public static implicit operator ActionResult<TValue>(Problem problem) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooh, this is nice.
/// Initializes a new instance of the <see cref="SerializableError"/> class. | ||
/// </summary> | ||
public Problem() | ||
: base(StringComparer.OrdinalIgnoreCase) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please no
@khellang there's a discussion about moving it further up the stack - aspnet/Diagnostics#346. Our plan was to get something going in Mvc and see if we can move the contract to diagnostics so it does the right thing for API responses. |
} | ||
|
||
return default(TValue); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is kinda a minefield. The 'dictionary' is mutable, so it's just as sensible to add the status code as a string, which would throw if you use the getter.
Should we just write a custom serializer for this?
namespace Microsoft.AspNetCore.Mvc | ||
{ | ||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] | ||
public class ProblemErrorPolicyAttribute : Attribute, IErrorPolicy, IExceptionFilter, IResultFilter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not paying too much attention to this type right now because it will be a bigger item to flesh this out. I want to focus on the O/M for problem
A problem with the dictionary approach and the way it's currently implemented (using |
Yep. We're starting here. |
@khellang - since you've been doing something similar do you have an O/M for problem that you like already? Wondering if we can borrow some inspiration... |
Not really much for inspiration... /// <summary>
/// Represents "Problem Details for HTTP APIs"
/// </summary>
/// <remarks>
/// See RFC 7807 - https://tools.ietf.org/html/rfc7807
/// </remarks>
public class ProblemDetails
{
public static readonly Uri AboutBlank = new Uri("about:blank");
/// <summary>
/// A URI reference that identifies the problem type.
/// </summary>
/// <remarks>
/// This specification encourages that, when dereferenced, it provide
/// human-readable documentation for the problem type (e.g., using HTML).
/// </remarks>
public Uri Type { get; set; } = AboutBlank;
/// <summary>
/// A short, human-readable summary of the problem type.
/// </summary>
/// <remarks>
/// It SHOULD NOT change from occurrence to occurrence of the problem, except
/// for purposes of localization (e.g., using proactive content negotiation).
/// </remarks>
public string Title { get; set; }
/// <summary>
/// The HTTP status code generated by the origin server for this occurrence of the problem.
/// </summary>
public int Status { get; set; }
/// <summary>
/// A human-readable explanation specific to this occurrence of the problem.
/// </summary>
public string Detail { get; set; }
/// <summary>
/// A URI reference that identifies the specific occurrence of the problem.
/// It may or may not yield further information if dereferenced.
/// </summary>
public Uri Instance { get; set; } = AboutBlank;
} I basically inherit from that to add properties specific to the different problem types. Something like public class ValidationProblemDetails : ProblemDetails
{
public ValidationProblemDetails()
{
Errors = new Dictionary<string, string[]>();
}
public IDictionary<string, string[]> Errors { get; }
} I then have an exception filter to map different exception type to different problem types, like I mentioned above. It's working pretty nicely for us 😄 |
var problem = new Problem | ||
{ | ||
Type = context.Exception.HelpLink, | ||
Title = context.Exception.Message, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this is best as a title. The RFC states
It SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization (e.g., using proactive content negotiation).
I'm not sure an exception message would qualify 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we could use a standard "An unknown error has occurred." That said, "SHOULD" is somewhat more lenient than "MUST", so maybe this is fine as it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we're not going to do what this code does today. We don't return exception messages to users in general.
a31063c
to
ce5f887
Compare
🆙 📅
|
If you're creating the |
d6fd641
to
d04c748
Compare
ce5f887
to
ba812b5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering a bit about the future of SerializableError
in this Problem
atic world…
[HttpPost("/problem")] | ||
public ActionResult<Person> Problem(int cooks) | ||
{ | ||
if (cooks > 10) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be 2
? 😸
{ | ||
public class ActionResultApiDescriptionProvider : IApiDescriptionProvider | ||
{ | ||
public int Order => -1000 + 10; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest a comment about the significance of this value.
set => AdditionalProperties[key] = value; | ||
} | ||
|
||
public IDictionary<string, object> AdditionalProperties { get; } = new Dictionary<string, object>(StringComparer.Ordinal); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be lazily-initialized with a property indicating it's been created? Not sure always creating the dictionary is much better than subclassing SerializableError
or another dictionary type.
Closing this in favor of focused set of PRs starting from #6742. |
No description provided.