Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

[Design] Add support for API error response type based on RFC 7807 #6695

Closed
wants to merge 2 commits into from

Conversation

pranavkm
Copy link
Contributor

No description provided.


namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
public interface IErrorPolicy
Copy link
Contributor Author

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.

Copy link
Member

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>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Support for Extension Members

Copy link
Member

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?

Copy link
Contributor Author

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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)
Copy link
Contributor

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.

Copy link
Member

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 😆

@khellang
Copy link
Contributor

khellang commented Aug 22, 2017

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 options.Handle<ValidationException>(ex => new ValidationProblem(ex)) where ValidationProblem is a derived type from Problem.

@@ -50,6 +50,17 @@ public ActionResult(ActionResult result)
return new ActionResult<TValue>(result);
}

public static implicit operator ActionResult<TValue>(Problem problem)
Copy link
Member

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please no

@pranavkm
Copy link
Contributor Author

@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);
}
Copy link
Member

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
Copy link
Member

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

@khellang
Copy link
Contributor

A problem with the dictionary approach and the way it's currently implemented (using nameof) is that you loose the ability to follow the user's preferred casing for JSON since it won't, by default, change casing for dictionary keys. This means that all JSON properties will always be PascalCased using this approach.

@rynowak
Copy link
Member

rynowak commented Aug 22, 2017

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.

Yep. We're starting here.

@rynowak
Copy link
Member

rynowak commented Aug 22, 2017

@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...

@khellang
Copy link
Contributor

khellang commented Aug 22, 2017

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,
Copy link
Contributor

@khellang khellang Aug 22, 2017

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 😉

Copy link
Contributor Author

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.

Copy link
Member

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.

@pranavkm pranavkm force-pushed the prkrishn/problem branch 2 times, most recently from a31063c to ce5f887 Compare August 23, 2017 17:50
@pranavkm
Copy link
Contributor Author

🆙 📅

  • Problem no longer derives from a Dictionary, so that's somewhat cleaner. I made it a property instead to stash arbitrary additional values. Derived types like the ones @khellang uses (sans the error mapping) is supported if you'd rather use strongly typed models. I tried playing around with dyanamic here but since all you do is assign stuff, the programming model doesn't seem nicer.
  • Wrote a custom Json formatter. Also works with Xml.
  • Need to figure out how to get it use the right Content-Type. Right now it says application/json. We want it to say application/problem+json.

@rynowak
Copy link
Member

rynowak commented Aug 23, 2017

If you're creating the ObjectResult you can put the content type there.

Copy link
Contributor

@dougbu dougbu left a 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 Problematic world…

[HttpPost("/problem")]
public ActionResult<Person> Problem(int cooks)
{
if (cooks > 10)
Copy link
Contributor

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;
Copy link
Contributor

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);
Copy link
Contributor

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.

@pranavkm
Copy link
Contributor Author

pranavkm commented Sep 7, 2017

Closing this in favor of focused set of PRs starting from #6742.

@pranavkm pranavkm closed this Sep 7, 2017
@dougbu dougbu deleted the prkrishn/problem branch July 15, 2018 03:47
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants