Skip to content

Return 404 or 200 on error/non existent resource #636

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

Closed
wants to merge 9 commits into from

Conversation

wisepotato
Copy link
Contributor

@wisepotato wisepotato commented Nov 27, 2019

Closes #631

Problem

Upon getting to a resource that is non-existent, the following object is returned:

{
  "links": {
    "self": "https://localhost/api/users"
  },
  "data": null
}

Proposed solution

When a 200 is applicable, i.e. the route exists, but nothing can be returned, i.e. when a user with id 1 exists and a GET request is sent to http://example.com/users/1/books. But it should return a 404 if user 1 does not exist, also for http://example.com/users/1.

We also require a descriptive error message, instead of just returning 404.

Done

  • Requesting an existing base model with no to-one relationships should return 200 with null for data. ( GET /api/v1/todoItems/412/oneToOnePerson)
  • [] Requesting an existing base model's relationships with a to-many relationship should return 200 with [] for data ( GET /api/v1/todoItems/1/stakeHolders)
  • Requesting an existing base model with no to-one relationships should return 200 with null for data. ( GET /api/v1/todoItems/412/relationships/oneToOnePerson)
  • Requesting an existing base model's relationships with a to-many relationship should return 200 with [] for data ( GET /api/v1/todoItems/1/relationships/stakeHolders)
  • Requesting an non-existing base model (todoItem) should return 404

@wisepotato
Copy link
Contributor Author

@bart-degreed anything you're missing?

Copy link
Contributor

@bart-degreed bart-degreed left a comment

Choose a reason for hiding this comment

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

See comments. I only tested getting a single missing item, for example /books/123.

responseContent = JsonConvert.SerializeObject(context.Object);
var requestedModel = _currentRequest.GetRequestResource();
var errors = new ErrorCollection();
errors.Add(new Error(404, $"The resource with type '{requestedModel.ResourceName}' and id '{_currentRequest.BaseId}' could not be found"));
Copy link
Contributor

Choose a reason for hiding this comment

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

The resource of type...
and should "id" be uppercase?

}
else
{
if (_getRelationship == null)
Copy link
Contributor

@bart-degreed bart-degreed Jan 6, 2020

Choose a reason for hiding this comment

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

This check is duplicated in both true and false blocks of the outer if statement. I believe it can be moved to the top, outside the if clauses, so it appears only once.
They are actually different: Relationship vs Relationships


namespace JsonApiDotNetCore.Internal
{
public class ResourceNotFoundException : Exception
Copy link
Contributor

Choose a reason for hiding this comment

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

This type appears to be unused and can be removed.

private readonly ILogger _logger;

public DefaultExceptionFilter(ILoggerFactory loggerFactory)
public DefaultExceptionFilter(ILoggerFactory loggerFactory, ICurrentRequest currentRequest)
Copy link
Contributor

Choose a reason for hiding this comment

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

The private field that is assigned from this parameter is never used.

@@ -110,6 +110,10 @@ public ResourceLinks GetResourceLinks(string resourceName, string id)
/// <inheritdoc/>
public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable parent)
{
if(parent == null)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think there should be a space between the if keyword and the opening parenthesis.


// lol
var relationshipValue = typeof(TResource).GetProperty(relationship.InternalRelationshipName).GetValue(entity) ;
var relEmpty = relationshipValue == null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused variable

if (entity == null)


// lol
Copy link
Contributor

Choose a reason for hiding this comment

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

Unclear comment

return null;
}
var listCast = (IList) relationshipValue;
if(listCast != null)
Copy link
Contributor

Choose a reason for hiding this comment

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

Expression is always true. And should test for IEnumerable instead of IList.

}

//if (entity == null)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why was this block commented out? Should it be removed instead?

@bart-degreed
Copy link
Contributor

I did some more testing today and found that this PR throws a NullReferenceException on many-to-many relationships in DefaultResourceService at the next line:

var relationshipValue = typeof(TResource).GetProperty(relationship.InternalRelationshipName).GetValue(entity) ;

in the debugger, relationship.InternalRelationshipName has value JsonTags (which is the json-exposed property, not what is backed by an EF Core entity. My model looks like this:

public sealed class Pathway : Identifiable
{
    [NotMapped]
    [HasManyThrough("tags", nameof(Tags))]
    public List<Tag> JsonTags { get; set; }

    public List<PathwayTag> Tags { get; set; }
}

where PathwayTag is the join-table and Tag the exposed resource. Used URL:

https://localhost/pathways/ABC/tags

So I tried to fix this by adding this:

            object relationshipValue;
            if (relationship is HasManyThroughAttribute hasManyThroughRelationship)
            {
                relationshipValue = hasManyThroughRelationship.ThroughProperty.GetValue(entity);
            }
            else
            {
                relationshipValue = typeof(TResource).GetProperty(relationship.InternalRelationshipName).GetValue(entity);
            }

this works, unless there is no match found. In that case, this method returns null, which causes another NullReferenceException raised at the last line of:

        // triggered by GET /articles/1/{relationshipName}
        public virtual async Task<object> GetRelationshipAsync(TId id, string relationshipName)
        {
            var relationship = GetRelationship(relationshipName);
            var resource = await GetRelationshipsAsync(id, relationshipName);
            return relationship.GetValue(resource);
        }

because resource is null, so getting the relationship value fails.

@bart-degreed
Copy link
Contributor

Hi @wisepotato I believe I fixed all issues as part of #714. See the tests in FetchingRelationshipsTests and the last two of UpdatingRelationshipsTests. Would you be okay if we close this PR? If I've missed anything, please let me know.

@bart-degreed bart-degreed deleted the fix/404-or-200 branch August 21, 2020 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Breaking change: null data returned when resource not found
2 participants