Closed (works as designed)
Project:
Drupal core
Version:
8.9.x-dev
Component:
jsonapi.module
Priority:
Major
Category:
Bug report
Assigned:
Issue tags:
Reporter:
Created:
8 Feb 2018 at 10:35 UTC
Updated:
26 May 2024 at 10:11 UTC
Jump to comment: Most recent, Most recent file
Comments
Comment #2
wim leersComment #3
wim leersAnd then we can go one step further…
Comment #5
wim leersIn #3, I forgot to update the code that throws
EntityAccessDeniedHttpExceptions.Comment #6
wim leersAnd this updated the expectations of
JsonApiFunctionalTest. Should be green.Comment #7
wim leersThis affects the DX of JSON API negatively today.
Comment #9
gabesulliceThis is much cleaner and makes more sense.
:)
Comment #10
e0ipsoIn a collection resource, if you have multiple errored entities, how would you know that a given error applies to a given resource?
Comment #11
gabesullice@e0ipso, can't we use the
links,sourceand/ormetakeys to identify it, where necessary?http://jsonapi.org/format/#error-objects
Comment #12
e0ipsoGabe I'm OK with that. Links seem to be the best way to identify a resource entity, but I don't have an opinion on that.
However I do not think we should just lose the functionality. 👍 or 👎?
Comment #13
wim leersWow, great catch!
On the one hand, I can't believe that the JSON API spec doesn't prescribe how to support that. On the other hand, I'm not at all surprised, because that is why you wrote
JSON API Partial Success.mdof course.It's increasingly looking like we really need to start interacting with the JSON API spec writers to get these significant/critical gaps in the spec addressed.
Two related observations I made while I was looking into this:
we put the errors underNever mind, apparently this would violate the JSON API spec — see https://gist.github.com/e0ipso/732712c3e573a6af1d83b25b9f0269c8#gistcomm... —{…, meta: { errors: […] }}, which violates http://jsonapi.org/format/#error-objects, that says it should be under{…, errors: […]}! Looks like we need an issue to fix that?I sure hope they'll change this, because right now that makes JSON API braindead wrt collection handling.
I agree, we should not lose that functionality, because otherwise it's not clear which resource the error is for. I see three possible solutions:
links.resource: a link pointing to the inaccessible resource's individual JSON API URL. Under aresourcekey perhaps?meta: we could then puttypeandidin there, someta.typeandmeta.idsource.pointer: if we would be including a resource identifier object for inaccessible resources, then we could use thepointerundersourceto its full potential: we could then specify not/datato indicate an inaccessible resource in a collection (which honestly is just completely wrong), but/data/2, assuming that the resource identifier object is at position 2 (third) in the collection.I much prefer that third possible solution. It feels most in line with JSON API's intent.
Comment #14
gabesulliceWow @Wim Leers, I really like #13.3 and I'm jealous I never thought of it lol
This would makes so much more sense for things like pagination too.
Comment #15
wim leersGlad you like it Gabe! :D It's actually in a big part thanks to you — you talked a fair bit about pagination lately, and wrote that handbook page that you asked me to review. :)
I just noticed something wasn't clear in my comment:
The intent I'm referring to is mostly this http://jsonapi.org/format/#error-objects:
The examples they give are for single resource URLs. But if you apply the same reasoning to resource collection URLs, you end up with
/data/2to point out that the third resource is not accessible, or perhaps/data/4/attributes/titleto indicate that the fifthPOSTedresource (when creating multiple resources at the same time) was not created because its title was invalid.IOW: use the power of JSON Pointers :)
Comment #16
e0ipsoAll of those options seem interesting. I'd like 3️⃣ slightly better, but all of them will work.
JSON Pointers are cool. JSON Path is great! :-P However in this case JSON pointer is more appropriate.
Comment #17
wim leersAlright, sounds like we have a plan! :) /me high-fives Mateu & Gabe!
Comment #18
gabesulliceI've created a dependent issue to start including resource identifiers instead of simply omitting items when access is denied: #2944348: Provide a resource identifier instead of omitting the resource when access is denied
That will allow us to include a
source.pointerin error objects as suggested by #13.3Comment #19
wim leers🎉
Comment #20
e0ipsoIs there extra work that needs to happen here when #2944348: Provide a resource identifier instead of omitting the resource when access is denied lands? If not we can simply mark this one as fixed instead of postponed.
Comment #21
gabesullice@e0ipso, the other issue was meant to only add resource identifiers instead of omitting them from the response. After that was finished, I planned on using this issue for changing the pointers.
However, after actually working on this, these two things (removing resources and generating errors) are pretty inextricably linked, so doing it in two issues doesn't make sense any longer. I'm going to close #2944348 in just a second.
The attached patch is the smallest possible set of changes that I felt I could introduce to make this work, but it does feel a bit hacked in. That said, doing anything else would probably mean a much larger patch and our efforts can probably be better used elsewhere.
Without further ado, "it works on my machine" so let's see what the testbot says :P
Comment #22
gabesulliceComment #25
gabesulliceComment #27
wim leersReviewing the test-only patch:
This was indeed incorrectly named.
This is the real change: inaccessible resources still get resource identifier objects.
And this is the other real change: error objects now point to the inaccessible resource identifier objects.
This is just refactoring.
This is verifying there's a resource identifier object as the inaccessible included resource.
And the one that is accessible then naturally needed to shift around.
All those comments are just voicing my understanding. Looks great! 👌
Comment #28
wim leersReviewing everything else in the "main" patch:
Rather than adding this new value object class, I'd modify the existing one. "access denied" (403) isn't the only reason a JSON API document's error object should contain a
pointer.Is there a reason you chose this approach instead?
This is a simplification we can now make because inaccessible includes now also always have a resource identifier object, right?
Comment #29
gabesulliceOne clarification: #27.4 is not just a refactoring. It previously would have failed because the
attributeskey wouldn't be defined on the resource identifier array. Sincearray_columnjust casts out arrays that don't have the column key, it avoids all theisset()checking one would otherwise have to do :)You can make a really neat (albeit naive) nested value extractor with that idea:
kinda nifty, no?
Comment #30
e0ipsoYour comment made me think of
lodash. I was terrified to find out about https://lodash-php.com.Comment #31
gabesullice#28.1
This change actually has little to do with pointers. This change needed to happen because entity access exceptions replace entities in an entity collection when they are loaded and checked for access. Because they replace the entities, we have to preserve identity information on the exception itself so that we can later normalize a resource identifier in the collection (previously, the exceptions were just filtered out). There's no other case where an exception needs to generate a resource identifier.
#28.2
That's correct.
Comment #32
gabesulliceRe: 28.1, you can see here that we previously didn't add a value to
datawhen we encountered an exception.Now, we need to rasterize the exception into a resource identifier, in addition to rasterizing the error.
Comment #33
wim leersYou're right that this is not per se related to pointers. But what matters here is that some exceptions involve existing resources, and the corresponding error objects may want to refer to those existing resources, even if we don't do that yet today. I'd say we shouldn't complicate today's patches for tomorrow's features (that may or may not happen), but in this case we're adding infrastructure and complexity that AFAICT would not be necessary if we did take tomorrow into account.
For example: a 409 when
POSTinga resource to a collection that has some uniqueness constraint that is being violated, for example creating aUserwith the same name, or aFeedwith the same URL — we'd want to be able to point not to the resource it's conflicting with. That would require a resource identifier (not indata, but in the error object).TL;DR: the complexity of adding a
EntityAccessDeniedHttpExceptionNormalizerand aEntityAccessDeniedHttpExceptionNormalizerValueseems higher than modifyingHttpExceptionNormalizerValue?(I'm not 100% convinced by neither your reasoning nor mine though — I don't feel very strongly about this, but I think it's our duty as reviewers to question architectural decisions.)
Comment #34
gabesulliceSoo... I was initially very gung-ho about this in #14. However, after working on this and writing extensive pagination and filtering docs, I'm no longer as excited about this as I once was. I am no longer am convinced that we should include a resource identifier for inaccessible resources.
Why? It relies on
meta.errorsto indicate that a resource is not accessible by the current user.meta.errorsis a non-standard feature of our implementation. We've tried to formalize it somewhat, but the fact remains that it is not part of the base spec. By including a resource identifier, there is no Drupal-agnostic way for a client to differentiate between a resource which was "forbidden" and a resource which simply doesn't have attributes/relationships.I was also excited about this making pagination simpler, because
page[limit]would then effectively operate aspage[size](meaning it would never have fewer than the limit number of resources when additional pages exist. I thought this would simplify client implementations, but I no longer thing that's true. While this would make it easier to reason about pagination, it wouldn't simplify client implementations because we're pushing the burden of removing resources from the UI into the client's domain... and their only way to do that will be to inspectmeta.errors(which would mean they have to be "Drupal-aware") or do some heuristic based on the presence/absence of fields.Finally, while I'm not sure this is technically a BC break, I think it will certainly cause unexpected behavior for many clients which have been relying on the backend to remove inaccessible resources from the
datamember of the document. This will be likely to cause JS errors wherever clients have made reasonable (yet optimistic) assumptions, like "every resource underdatawill have aattributes.titlekey".So, what next?
I think we need to reevaluate adding individual errors for inaccessible resources altogether. If we're concerned about DX, then what we should be most concerned with is reducing the signal to noise ratio and providing actionable information. Individual errors do not satisfy that need. Instead, I propose that we provide a single error when any resource is omitted from the response:
I think this (1) reduces the signal to noise ratio, (2) is actionable, (3) is useful even when inactionable (e.g. providing a UI indication that items have been removed, even when they can't be filtered out).
I'm completely open to changing the langauge/contents of the error object. I'll point out that I chose "Insufficient Privileges" to distinguish it from "403 Access Denied" and I would be (reluctantly) open to including a meta object in the error object itself with links to the omitted resources. Like so:
Why not include those links in the parent error object? Because the spec isn't clear about whether we can or can't add additional members to the error object's link object."
However, I think it really doesn't serve a very useful purpose. I'm open to being convinced otherwise though.
Comment #35
wim leersI appreciate that, but the JSON API spec version 1.0 does not deal with partial success at all. The JSON API module for Drupal already violates this principle out of sheer necessity: without it, there's no way to make a sensible API. (This is the #1 gap/oversight in the JSON API spec IMHO.)
This is an excellent point — you're right that it almost certainly has BC implications.
Hm … not sure yet. This is where I feel obligated to point to the JSON API spec again: this is surely such a common need except for the most trivial of applications, yet it's completely unmentioned. Error handling/handling of inaccessible resources seems like one of the areas where JSON API libraries can make the biggest DX difference to client developers! But alas …
I'm not sure what to do. This is where JSON API disappoints, sadly :(
Comment #36
e0ipsoMy strong feelings regarding the (brilliant) argumentation @gabesullice made are:
Accept: application/vnd.api+json; extensions="fancy-filters,partial-success", instead ofAccept: application/vnd.api+json, to formalize that awareness."pointer"doesn't work for us, we can find a way to add a JSON Pointer to the appropriate entity. Maybe:meta.externalPointer -> https://example.org/jsonapi/node/article/1234-abcd#/data/attributes/title. Whatever we do, we add it to the Partial Success extension and it becomes as formal as if it was in the base spec.If we want to have the digested error added on top of that, and then detailed errors hidden from the the average user via a permission; I don't feel strongly about that.
Comment #37
e0ipsoTo re-iterate, this is the only wording preventing us from having a pointer to an external JSON document in the
source.pointer:I wouldn't oppose being practical and having a JSON pointer that is not restricted to the requested document. Even if that is not 100% in line with the base spec.
Comment #38
gabesulliceI think I see where the mental leap is that I failed to state explicitly. I think the single greatest distinction in my mind vs. your minds is now this:
An entity removed from a collection for access reasons is not an error, nor is it exceptional. It is expected behavior.
Everything else follows from that.
How do we know this is true? Let me show it by example:
If a node is not present in a query result because of node grants, Drupal doesn't throw an exception, it silently removes the result. It's not an error. It's expected behavior.
In the PHP API you may override that behavior, but it's still the default. Since this is a public API, we cannot override it, but that doesn't change the principle.
If you join me in my thinking above, then "partial success" is not relevant because nothing failed. There's no need to even concern ourselves with extensions and other whatnot in that regard.
Neither do I! That is why I explicitly said:
"I would be (reluctantly) open to including a meta object in the [digested] error object itself with links to the omitted resources."
And I even included an example of how we might do that :) If clients wish to know the reason that access was denied, they may follow the link, where they'll receive a status, reason, and detailed information.
Comment #39
wim leersThis is a really good point. We should mention this in
jsonapi.api.php. But at the same time, we need to recognize the fact that we're not going to be able to generate responses that don't also adhere to those extensions. So perhaps even more importantly than documenting that this should be theAcceptrequest header, we should update our response headers. But interestingly, http://jsonapi.org/extensions/ says an "extension system is under development" yet points to https://github.com/json-api/json-api/blob/9c7a03dbc37f80f6ca81b16d444c96..., which used to be the extension system. We really need to get this right.Comment #40
wim leers#38: I'm interested to hear Mateu's thoughts on this, my thinking on this hasn't fully crystallized yet.
Comment #41
wim leersPer #34, reopened #2853066: #2853066-12: Spec Compliance: Inaccessible collection/related resources surface errors: should be 200 with hypermedia + metadata.
Comment #42
wim leersI re-read this issue, because it's been around for a while.
That of course ended with reading the last comment on this issue not written by me: @gabesullice's comment in #38. 2.5 months later, I still find myself going "hmmm, that's right" and then nodding when reading
This issue is absolutely hard-blocked on @e0ipso. And I think it's very important for the future of JSON API. Bumping to priority and assigning to @e0ipso.
Comment #43
wim leersAlso: we reached consensus at #2853066-13: Spec Compliance: Inaccessible collection/related resources surface errors: should be 200 with hypermedia + metadata a few days ago! I think that could help get us to consensus on this too?
Comment #44
wim leersThis is also hard-blocking #2857730: Add a link to get only published entities in collections.
Comment #45
e0ipsoWe don't treat entities differently depending on if they are in a collection or individual resources. This suggests that we do here. If an individual request to an entity fails with an error, then I think that the same operation as part of a collection should also be seen as one. I believe (by the accumulation of reports in that sense) that many assume the same, and it would be very confusing to treat it as success or error depending if it's a collection or not.
I do believe that things have partially failed if you request a collection where some entities fail.
Comment #46
e0ipsoAlso, I remember when researching this in the past that all the questions and approaches in this matter (in the forums) acknowledged the existence of an error of an error.
Comment #47
gabesulliceWe respond with a 403 error for an inaccessible individual resource. 400-level errors are client errors. The error is accessing the individual resource (resource as in route, not as in entity) with insufficient authorization. IOW, the entity did not fail, the authorization did.
In #2853066-13: Spec Compliance: Inaccessible collection/related resources surface errors: should be 200 with hypermedia + metadata, we agreed that even a collection with 0 results (for access reasons) is a 200. Access to the "collection resource" is a success (200).
In a collection with omitted entities, I think it's correct to return a single "Insufficient Privileges" error that mirrors the client error at the individual resource level, with extra metadata about which resources would be viewable if those additional privileges were attached to the request. That's consistent with the consensus we reached (reflected in the IS update at #2853066-18) for the 2.x branch. This complements the idea that you might want to "correct" your request by adding a
?filter[status]=1filter to your query, since it was a client error, not an entity error.Given that #2853066, deals with a superset of the issue here and that we're trying to freeze the 1.x branch and release 2.x very soon, I propose we simply close this issue as "works as designed" for the 1.x branch, ignoring any spec violation WRT to the error ID in 1.x. It'll be a non-issue in 2.x.
Comment #48
e0ipsoComment #49
rpayanmMoving to Drupal core's issue queue.
I'm working on https://www.drupal.org/project/drupal/issues/3122113
Comment #50
bbralaHmm, if this works as designed, we might need to move along and try and execute:
/Users/bjorn/projects/drupal/core/modules/jsonapi/tests/src/Functional/NodeTest.php:341. Which is a todo to remove some code when this issue is closed. Opening a child issue.Comment #51
bbrala#3449891: Move to new test path in NodeTest as per todo.