Problem/ Motivation
Provide context to the client when updating a resource with PATCH by returning the complete entity.
Description
PATCH applies a diff to the requested resource; rather than replacing it. This can mean the client is not always aware of the complete resource being updated. The server also has the opportunity to complete additional processing on the entity which the client might need to be made aware of. While providing meta information of the updated entities location or id is valid it still requires the client to perform an additional GET if the complete resource is needed.
Proposed resolution
Return the complete entity.
Comment | File | Size | Author |
---|---|---|---|
#65 | interdiff.txt | 1.26 KB | tedbow |
#65 | 2662284-65.patch | 21.85 KB | tedbow |
#58 | rest_post_entity_body-2662284-58.patch | 22.84 KB | Wim Leers |
Comments
Comment #2
swim CreditAttribution: swim commentedFirst attempt -_^
Comment #3
swim CreditAttribution: swim commentedComment #5
Wim LeersMakes sense, though a HTTP/REST expert should verify this is okay.
Comment #6
Wim LeersThe sister issue #2546216: Return entity object in REST response body after successful POST landed. Let's get this done for 8.1 too!
Comment #7
swim CreditAttribution: swim commentedOki doki =), this patch addresses the obvious (pre-existing) tests. Not sure how we should handle the testPatchUpdate function E.G. Should we check the returned entity as well as loading the entity again; to ensure both validate? See below.
Comment #8
Wim LeersYour code example looks good. Perhaps on top of that also do what #2546216: Return entity object in REST response body after successful POST did: verify the UUID is in the response.
Thanks for pushing this forward! :)
Comment #9
swim CreditAttribution: swim commentedThanks Wim =)!
amended patch with your suggestions. Hopefully the test text responses are okay :S. Not sure if re-loading the entity is required now but more asserting can't hurt.
Comment #10
Wim LeersNext time please include the interdiff: https://www.drupal.org/documentation/git/interdiff :)
The quoted changes are actually form the interdiff.
Comment #11
Wim LeersI think the updated test coverage is sufficient now. RTBC as far as I'm concerned!
I just would like sign-off of this from someone with deeper REST knowledge though. So not just yet RTBC'ing.
Comment #13
dawehnerIs it a "api" break to change the status code? I guess noone really can actually rely on this behaviour?
Comment #14
Wim Leers#13: it's an API break to the same extent that we were returning an empty body before and now not anymore. HTTP required us to use 204 for HEAD's code, and with this change, it requires us to use 200.
Comment #16
dawehnerA quote from https://tools.ietf.org/html/rfc5789
Given that, the idea of the patch is perfect.
Comment #17
Wim LeersExcellent! Now we just need to get the patch green then.
Comment #18
dawehnerThere is a potential BC break when client code actually checks for this exact return code. On the other hand I think the risk is negligible.
Comment #19
Wim LeersAgreed. And usually it means they can remove an extra round trip.
Comment #20
Wim LeersThat being said, this needs a CR, just like #2546216: Return entity object in REST response body after successful POST needed one: https://www.drupal.org/node/2665276.
Comment #21
Wim LeersCR created: https://www.drupal.org/node/2733655
Comment #22
Wim LeersLet's make this comment be in line with the comment that #2546216: Return entity object in REST response body after successful POST added to
::post()
.The left-hand side of this assertion is wrong, which is why this test is failing. I think it's clearer to make this just like the assertion added in #2546216: Return entity object in REST response body after successful POST:
I know I asked for the UUID to be checked because that's what #2546216: Return entity object in REST response body after successful POST tested. But that actually does not make a lot of sense: when POSTing (like in that other issue), a UUID is generated. When PATCHing (like here), no UUID is generated, it remains the same. So, it makes more sense to just verify that the updated field was actually updated. You're already testing that above.
Unnecessary change.
Addressed all my feedback.
Comment #23
dawehnerIMHO we should have at least one test which tests the entire response, rather than just a small subset of it.
Comment #24
Wim LeersFair!
Comment #25
Wim LeersFirst, a rebase, because #2626298: REST module must cache only responses to GET requests landed and conflicted with this.
Comment #27
Wim LeersSo, there was one new PATCH assertion that is still causing that one fail in #25. This fixes that.
Comment #29
Wim LeersAnd now this morning #2631774: Impossible to update Comment entity with REST (HTTP PATCH): bundle field not allowed to be updated, but EntityNormalizer::denormalize() requires it landed, which again expanded test coverage for PATCHing, and hence causes yet a different failure now, because this patch did not yet update it. This fixes that.
Comment #31
Wim LeersAnd this time the failure is due to details, and I cannot reproduce it locally :( Anyway, the failure is because #29 already tries to verify the entire response, like #23 requested. Because since #2631774: Impossible to update Comment entity with REST (HTTP PATCH): bundle field not allowed to be updated, but EntityNormalizer::denormalize() requires it, we can verify the entire response not just for patching an
entity_test
entity, but also for acomment
entity.As #2631774-25: Impossible to update Comment entity with REST (HTTP PATCH): bundle field not allowed to be updated, but EntityNormalizer::denormalize() requires it already indicated, there is extreme brittleness in JSON vs HAL+JSON, and this is just further proof of that.
So, let's revert most of #29's interdiff, and do the absolute minimum: verify that it's a 200 response now instead of a 204, and don't verify the entire response.
Comment #32
Wim LeersI added #31 to #2737719-2: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method.
Comment #33
Wim LeersSo, at long last, finally, this patch addresses #23!
#23: Done.
However, in doing so, I encountered yet another bug in REST:
That comment is plain wrong. We only have
_format
set for the GET route, not for the PATCH/POST routes (see\Drupal\rest\Plugin\ResourceBase::routes()
). Which means that you ALWAYS get ajson
response for PATCH/POST, even if you askxml
or, like in this case,hal_json
!!!!!!!! And this is why verifying the entire response doesn't work, because we don't get HAL+JSON back, but just JSON!This code was always wrong: it would also not have worked in the days we were using
Accept
headers!So, to fix that, I added a
getResponseFormat()
helper method to determine the format to use for the response.After having done that, I encountered a new problem:
RequestHandler::handle()
currently always serializes the data, even if there isn't any (i.e.$data === NULL
). In HEAD, and due to the code quoted above, which always defaults to'json'
, the response to a DELETE request (which has no data to send in the response) will always end up serializing thatNULL
to the'json'
format. Which works (it results in nothing), but then with my fix above in place, no response format is determined (since the route for DELETE doesn't list any acceptable format, due to\Drupal\rest\Plugin\ResourceBase::routes()
's logic), which means it tries to serializeNULL
to the''
format, which does not exist. However… why are we even trying to serializeNULL
?! That makes no sense at all. So, this interdiff makes\Drupal\rest\RequestHandler::renderResponse()
a little less braindead.And from there, I found even more problems with REST's routing. Solving them all would be vastly out of scope here, so I opened #2737751: Refactor REST routing to address its brittleness for that.
Comment #35
dawehnerI'm wondering whether we could put that into the routing layer itself, rather than implementing a one-off implementation for REST.
Comment #36
Wim Leers#35: I agree with that in principle. Which points to the real problem: the REST module does not really use the routing system, or doesn't use it properly, and does much of its routing on its own, in its controller. See #2737751: Refactor REST routing to address its brittleness for fixing that. That is a much bigger undertaking. I'd much rather land this now, because it's a clear improvement over the current implementation, and then do #2737751 next.
Comment #37
Wim LeersThere were 2 bugs in my code, and one significant bug in the existing test coverage.
Two bugs in my code:
getResponseFormat()
did not yet handle_format
and_content_type_format
correctly in case they did not existhandle()
's changes failed to deal correctly with NULL (and it was needlessly repetitive too)The significant bug in the existing test coverage:
'_format' => 'json'
was being specified as a route default instead of a route requirement. Why did this go unnoticed? That's right… because of #33: because it just always picked JSON as the response format as the fallback…Comment #38
Wim LeersComment #40
Wim LeersOne more edge case: DELETE routes don't have any specified format. It just used to always fall back to
'json'
, which is why it "worked" (as explained in #33). Thanks to the increased strictness ofgetResponseFormat()
, we now discovered this edge case.Comment #41
dawehnerI was worried about heuristics for cacheable GET requests, but I think the code returns always the same as Content-Type doesn't vary itself.
I'm wondering whether we need tests for all these "new behaviour", or whether we have a lot of implicit tests already.
Comment #42
Wim LeersWe have a lot of implicit tests already. The fact that we don't need to modify any request URLs or request headers in tests is proof of that.
Comment #43
dawehnerGiven that we have a lot of implicit test coverage seems enough for me.
Comment #44
Wim LeersTo add to #42: #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method will be where I exercise all these edge cases in complete test coverage: I'll mimic a developer trying things out.
Comment #45
Wim LeersNote: this blocks #2664780: Remove REST's resource- and verb-specific permissions for EntityResource, but provide BC and document why it's necessary for other resources, see #61 there.
Comment #46
alexpottDo we have full test coverage of the fallback ability of this method - I can't spot it in the test changes.
This can be simplified to
If the requirement does not exist it returns NULL and explode will then just return an empty array as desired.
Why don't we return 'json' as before?
Comment #47
Wim LeersIt will output:
DELETE
, there is no response format, and there is no data to serialize, hence havingNULL
as the format is fine.Hence this, among other related changes. See #33 for details.
Yes, these patches look confusing and less than ideal. But that's because they're trying to bring sanity to utterly broken code. For most components, I'd agree with you, because most components are in better shape.
Comment #48
alexpottSo I think we should have explicit test coverage of the fallback as this is a logic change.
Comment #49
alexpottComment #50
Wim LeersFirst rerolling. This conflicted with #2308745: Remove rest.settings.yml, use rest_resource config entities.
Next: the explicit test coverage @alexpott is asking.
Comment #51
Wim LeersForgot the actual patch. Oops.
Comment #52
dawehnerLet's be honest :)
Comment #53
dawehnerComment #54
Wim LeersEh yes, that's what I'm working on — from #50:
I forgot to update the issue status here. Sorry.
Comment #55
Wim LeersTurns out that in order to have sane testing, i.e. without having to set up a lot of normalizers, making the container aware of those and so on and so on, we need #2419825: Make serialization_class optional. Because that allows us to just send request bodies that don't need to be deserialized into an instance of a class. Because that'd be testing something we don't need to test when testing
\Drupal\rest\RequestHandler::getResponseFormat
. It'd make that test far more complex than necessary. We just need to test the response format. And #2419825: Make serialization_class optional is what allows this patch to test that in a far easier manner.So… attaching a patch that includes #2419825 to prove that the test coverage works. And also attaching a do-not-test patch without it, because we should first land #2419825 separately.
You wanted tests? I'm glad you insisted, because this means at least this one low-level area of
rest
now has comprehensive, solid test coverage! I was also able to improve the code slightly in the process :)Comment #56
Wim LeersComment #58
Wim LeersWow, #2419825: Make serialization_class optional already landed!
Here's a rerolled patch, with only one change, to address the new failure. This one change is necessary because
\Drupal\Tests\rest\Kernel\RequestHandlerTest::testBaseHandler()
was using the same Route object for both GET and PATCH requests, which is not how the REST module actually works. GET specifies a_format
requirement, PATCH specifies a_content_type_format
requirement. By correcting that, the tests pass again, as expected.Note that #2419825 has been committed by Alex Pott, but not pushed. So this patch will fail. Once it's pushed, we can re-test it here, and it should pass :)
Comment #60
Wim Leers#2419825: Make serialization_class optional was pushed now, re-testing.
Comment #61
dawehner+1 for that
Honestly, its quite weird to test our tests. This is not really adding test coverage IMHO and on top is quite confusing
Comment #62
Wim Leers#61.2: Happy to remove those if you want. But time and time again I've seen future additions to data providers introduce more allowed types (start with only string, but then also allow FALSE, NULL, arrays), and that has caused subtle problems. That's why I added explicit assertions there.
If that's your biggest concern, then yay.
Comment #63
Wim Leers@dawehner Can you indicate what you think is blocking RTBC?
Comment #64
dawehnerAs we talked on IRC let's remove the
assert()
statements in core to not confuse people.Comment #65
tedbowA patch removing assert statements per #64
Comment #67
tedbowHmm that is very strange test to fail. Test is fine locally. Re-testing
Comment #68
tedbowOk, not sure why that test failed first time. Back to Needs Review
Comment #69
Wim Leersdawehner RTBC'd in #43, alexpott un-RTBC'd asking for explicit test coverage, which I added in #55. @dawehner was +1 in #61, had only one minor remark that @tedbow addressed in #65.
So, back to RTBC.
This has been ready for a month. Let's get it in.
Comment #70
alexpottCommitted 2bb0468 and pushed to 8.2.x. Thanks!
Comment #72
Wim LeersYay! This means one blocker less for #2664780-62: Remove REST's resource- and verb-specific permissions for EntityResource, but provide BC and document why it's necessary for other resources!