Problem
I'm having an issue trying to send a HEAD request to a D8 installation to verify the existence of a particular entity. A HEAD request to an existing (newly created) Node will always fail with the following 500 error:
LogicException: The controller must return a response (null given). Did you forget to add a return statement somewhere in your controller? in Symfony\Component\HttpKernel\HttpKernel->handleRaw() (line 157 of /var/www/drupal/vendor/symfony/http-kernel/HttpKernel.php).
Oddly, after sending a single GET request to the same endpoint all subsequent HEAD requests are successful. The handleRaw method in the error doesn't seem helpful as it only shows that a request object is not received.
How to reproduce
I was using HttpClient in a custom module, but the easiest way to reproduce is using Postman:
1. Create a new node setting Authentication, Accept, Content-Type, and CSRF token headers:
POST: http://drupalvm.dev/entity/node?_format=hal_json
With the following body:
{
"type": [{
"target_id": "article"
}],
"title": [{
"value": "testing head request to test entity exists",
"lang": "en"
}],
"status": [{
"value": 1,
"lang": "en"
}],
"promote": [{
"value": 1,
"lang": "en"
}],
"sticky": [{
"value": 0,
"lang": "en"
}],
"body": [{
"value": "this is the body < \/p>\r\n",
"format": "basic_html",
"summary": "",
"lang": "en"
}]
}
Returns 201 Created
2. Send a HEAD request to that new node (in this case nid 46):
HEAD http://drupalvm2.dev/node/46?_format=hal_json
Returns 500 server error
3. Send a GET request to that new node:
GET http://drupalvm2.dev/node/46?_format=hal_json
Returns 200 OK
4. Now repeat number 2. This and all subsequent HEAD requests are now successful with a 200 status.
| Comment | File | Size | Author |
|---|---|---|---|
| #82 | 2752325-65-8.1.x.patch | 5.15 KB | wim leers |
| #82 | 2752325-65.patch | 5.12 KB | wim leers |
| #75 | 2752325-75.patch | 5.65 KB | dawehner |
| #73 | 2752325-73.patch | 943 bytes | dawehner |
| #11 | interdiff.txt | 1.67 KB | dawehner |
Comments
Comment #2
ericpughComment #3
dawehnerOh interesting, this is a bug in
page_cachemodule ... it should include some logic for HEAD requests maybe.Comment #4
dawehnerComment #5
wim leers#3: I'm not sure it's a bug in
page_cache, I suspect it's thanks topage_cachethat subsequent HEAD requests work.I think the bigger problem is that apparently HEAD requests do not always work. Which is yet another reason for #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method.
Thanks for reporting this bug!
Comment #6
wim leersAdded this to the top priorities: #2721489-17: REST: top priorities for Drupal 8.2.x.
Comment #7
dawehnerWell, right, but let's assume we are outside of rest, but rather in some other custom code. page_cache should still not cache act on those HEAD requests like that.
Right, so this is an additional problem on top.
Comment #8
wim leersThe HTTP spec does not agree with you, https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4 says:
In other words, the
Cache-Controlheaders persist, so the response to a GET request is also valid for the HEAD response, it just needs to strip the response body. That's it.So Page Cache works correctly IMHO AFAICT, it's just that REST module doesn't support HEAD requests properly.
Comment #9
wim leersThe reason it doesn't work in the REST module is simple: none of our
@RestResourceplugins implement ahead()method. Therefore, you cannot make HEAD requests. That's it.So the question is very simple: do we require that
@RestResourceplugins implementhead()(so they can minimize the amount of data fetching, access checking …), or do we automatically implement it?I think it's best to require explicit implementation.
For
EntityResource, that'd require an addition like:i.e. this doesn't need to do all of the field-level access checking that the
get()method must do.Comment #10
wim leersComment #11
dawehnerWell, there is nothing inside page cache which ensures that the output varies by HTTP method. The interdiff provides an example route to show an example. Once you have cached the HEAD request, it will stick in there, even if you ask for GET requests. For me this is a bug of the page_cache module, which is 100% independent from whatever REST is doing.
Steps to reproduce:
In the other usecase, when we have a cached GET request, we could totally provide a fast HEAD request inside the page_cache, as we would just have to strip the content and call it a day.
Comment #12
dawehnerIMHO we should implement a default implementation and then let people figure out alternative better implementations.
Comment #13
wim leersWell, that sure is a bug. Great catch :) Filed #2753741: Page Cache caches all safe HTTP methods (GET+HEAD), but generates the same cache ID for either: add test coverage to prove this is correct, with an initial patch.
Comment #14
wim leersComment #15
dawehnerHere is an implementation of HEAD responses in REST module using existing GETs.
Comment #17
wim leersSee #9: #15 has one big downside: it does too much work. And in doing so, it can make the result uncacheable — e.g. when all users can view an entity (cacheable HEAD response), but cannot view all fields within the entity (uncacheable GET response).
Comment #18
dawehnerYeah I focused more on a generic implementation not just for entity resources but the generic usecase.
Comment #19
dawehnerThis should apply now
Comment #20
wim leersLooks great :) Now just needs tests.
Comment #22
dawehnerThis patch totally applies for me.
Comment #24
dawehnerHere is a quick fix. @marthinal might work on adding tests and adding a entity specific implementation.
Comment #25
marthinal commentedLet's see what the testbot says.
Comment #26
marthinal commentedComment #27
marthinal commentedFrom Symfony\Component\Routing\Matcher\UrlMatcher
Permissions for HEAD don't work. Basically HEAD is treated as GET AFAIK. So I think that we can use our new ModifiedResourceResponse class. IMHO makes no sense to cache a HEAD request because we are caching the GET request with an empty body.
Needs test!
@Wim @dawehner what do you think about that?
Comment #28
wim leers#27: can you provide an interdiff? It's not clear to me what the difference is.
Comment #29
marthinal commented@Wim yes sure. Attached!
Comment #30
dawehner@marthinal
Its sad that you removed the generic implemention in ResourceBase ... its quite nice that one doesn't have to develop it for other rest plugins. Do you mind adding it back?
Comment #31
marthinal commented@dawehner Sure. Sorry! I thought we only want the entity specific implementation. Missing integration tests. I'll try to continue working on it later.
Comment #33
marthinal commented1. Adding Integration test.
2. Removing
$response->setContent(NULL);from the generic implementation. By default we set the content to NULL here:Symfony\Component\HttpFoundation\Response
3. I think we don't need an entity implementation here. At least to be honest I'm not sure how to reproduce the situation explained here #17. But probably I'm missing something :)
ANyway HEAD is a GET but in the Response we remove the content.
Comment #34
dawehnerCan we document that responses handle that automatically by default?
Well, IMHO having the entity specific one would be fine, as its faster
Comment #35
marthinal commented1. Done.
2. Sure! Done
Thanks!
Comment #36
marthinal commentedThe first HEAD request is not cacheable because we are using
ModifiedResourceResponse.Comment #38
dawehnerIMHO this is semantically wrong. Those requests are totally safe to use and by that we can add the cacheability metadata.
Comment #39
wim leersGenerates a HEAD response by reusing the existing GET implementation, if any.
return (new ResourceResponse())->addCacheableDependency($entity_access);"for" or "by"?
More importantly: we have no test coverage for this.
We never use
$entity. So then we can just do:Hm, not a big fan of the use of
internal:here, but it's probably ok.What's the point of doing the same thing twice? Shouldn't this then verify
X-Drupal-Cacheheaders?Or shouldn't one of them do a GET request?
Comment #40
dawehnerWell, this clashes a bit with #2753741: Page Cache caches all safe HTTP methods (GET+HEAD), but generates the same cache ID for either: add test coverage to prove this is correct if we want to test it properly :)
Comment #41
wim leersRight! Thanks :)
Comment #42
marthinal commented39.1 Done. Doc comment short description must be on a single line. Removing "if any"
39.2 Done
39.3 Done
39.4 Done
Missing 39.6. I'll try to do it later.
Comment #44
wim leersOne space is missing :P
Comment #45
marthinal commentedOops @Wim++ :)
Ok! So IMHO the entity implementation should return
ModifiedResourceResponse(). In this test we can see that the first HEAD (MISS) is correct but the next request using GET (HIT) is not correct because the body is empty. So this test should fail. And... I think it makes sense because a HEAD is a GET but in the Response we remove the content.Comment #46
dawehnerIf someone can explain me this ModifiedResourceResponse for HEAD? HEAD is an independent operation, there is nothing to modify here ... I mean we went with using ModifiedResourceResponse and not something like NonGetResourceResponse, because there is HEAD.
Comment #48
wim leers#46++.
The failure here is not related to using
ModifiedResourceResponseinstead ofResourceResponse. In fact, the test even shows that: the test expects the presence of certain cache tags, butModifiedResourceResponsecannot even carry cache tags!Comment #49
marthinal commentedRerolled.
@Wim We have 2 different failures there(#45). And we are using
ResourceResponsethere.'entity_test:1' is present in the X-Drupal-Cache-Tags header.Value NULL is equal to value 'entity_test'.Removing the assertion about cache tags in the current patch(simply to show the problem). IMHO we are caching the HEAD response and then the GET response will have the body empty and
x-drupal-cachewill be set to HIT. And this is a bug because we want to obtain the body for GET requests.So using
ModifiedResourceResponsewe avoid to cache HEAD...Am I missing something here???
We can use the
Responseclass directly... then we avoid to cache and to use theModifiedResourceResponseclass...Attached 1 patch that should fail to see the error + the new patch using
Responseclass + the interdiff to see the difference.thanks!
Comment #51
dawehnerWhy do we want to avoid caching HEAD?
Comment #52
marthinal commentedWe don't want to avoid caching HEAD. :) Using ResourceResponse the test 2752325-49-sould-fail.patch fails because the body (for GET requests) is empty when doing a HEAD and later a GET.
So something like this will fail:
On the other hand if we do a GET and then a HEAD, the next GET will be correct. Something like this:
There's a related issue. #2753741: Page Cache caches all safe HTTP methods (GET+HEAD), but generates the same cache ID for either: add test coverage to prove this is correct
Maybe this is a problem with the cache system...
Comment #53
dawehner@marthinal
What about opening up a follow up to look into that again after #2753741: Page Cache caches all safe HTTP methods (GET+HEAD), but generates the same cache ID for either: add test coverage to prove this is correct went in?
Comment #54
wim leers#53: wrong issue link?
Comment #55
dawehnerThis is what happens if you try to not copy and paste stuff.
Comment #56
marthinal commented@dawehner To reproduce the problem we should apply the current patch otherwise HEAD is not working by default. So IMHO we need fix the problem here.
Testing a little bit more I found that this test works as expected:
I mean we are caching per url...
This is a very important comment https://www.drupal.org/node/2753741#comment-11325449
To be honest I can only think 2 different possibilities:
1) We don't need to implement HEAD for EntityResource. So we are caching the GET response and then Symfony\Component\HttpFoundation\Response will remove the content by default:
Drupal\rest\Plugin\ResourceBase
2) We want to implement HEAD for ENtityResource. Then IMHO we need to avoid caching the Response because a HEAD response will be cached with an empty body (makes no sense the return the body here because we already have GET). And makes sense because AFAIK we're caching per url.
@Wim @dawehner what do you think about that? I need your opinion here.
Comment #57
wim leersI'm going through this patch now. Trying to fix my nits at the same time, because @marthinal said at #2291055-184: REST resources for anonymous users: register that he doesn't have time for this this week, and this probably needs to land before feature freeze.
Missing docs. Fixed.
Not just HEAD, but also GET. If it wouldn't support HEAD, and only HEAD, then that'd be a severe bug. So, should mention both.
This had me confused at first. What you meant is not "by default", but "automatically" :) And by "preparing", you were referring to
Response::prepare(). So, let's make that more explicit. Fixed.We're assigning something here only to return it. Why not just do that right away?
Comment #58
wim leersWhy would you retrieve a passed in function argument like this? Why not just be explicit, just like
EntityResource::get()?Oh… if you change this like I would like to see:
… then you'll get this error:
And so that is why you did this.
I think
ResourceBase::head()is unacceptable if it means REST resources cannot be written in a correct manner anymore. Even more so because it is just a base class.This made me understand what @marthinal was getting at in #27 (and why he removed this method in that reroll): because Symfony and Drupal treat GET and HEAD as the same internally (it maps both to the same route). And so there is literally no point in having an optimized
head()method, because Symfony will still callget()anyway!This reroll
Attached is a reroll that:
EntityResource::head()to print a messageHEAD, that verifies that there's no response body (which would FAIL ifEntityResource::head()would actually be called!) and verifies that all headers are identical to the GET responseComment #59
wim leersSo, given that #58 proves that adding a
head()method to REST resource plugins is utterly pointless, I've:ResourceBaseEntityResourceComment #60
wim leersThis is not something we should implement in
rest.module. It's something the routing system should provide for us.And it will, once #2659070: REST requests without Content-Type header: unhelpful response significantly hinders DX, should receive a 415 response lands. It's currently RTBC.
So, let's remove all this and focus just on HEAD request support.
Comment #63
dawehnerJust a random / offtopic thought. We could put this generic HEAD support into the routing layer or somewhere there. This would allow us to support HEAD basically everywhere and we don't have to deal with this issue described in #58
Comment #64
wim leersTurns out I made two mistakes in #59:
HEADrequests. And if you implement a route that supports it, then that route will be picked over GET routes.However, that just means that the "proper" solution just looks different from what this issue had been doing so far. This is what AFAICT is the correct, and most Symfony-esque flow, that closely matches the existing expectations of Symfony:
PageCache::handle()caches the GET response (i.e. including body)DrupalKernel::handle()receives the GET response and calls$response->prepare()on it, which transforms a GET response to a HEAD response: it strips the body and updates theContent-LengthheaderThe end result: HEAD support! A HEAD request will cause the PageCache to be populated with a GET response. Which makes total sense when you take RFC2616 into account. And to get this, all we need to do is Use Symfony As Intended™!
Comment #65
wim leersThis is actually no longer necessary.
This specifically tests REST + PageCache.
This implicitly tests PageCache, but that's not the point. The What it really does test is that the responses are the same, as RFC2616 demands.
As far as I'm concerned, this is RTBC. But I've worked too much on it at this point to RTBC it.
Comment #66
wim leersAnd to prove that this is really the only change that is necessary… here's a patch identical to #65, but also a test-only patch that will fail. (No interdiff because no changes
Comment #68
tedbowThis looks good! RTBC!
Comment #69
dawehnerI still have the feeling that this could be even done on the routing level or so, and make it more generic than just for rest module. It could be also useful for jsonapi for example, or whatever.
Comment #70
wim leersHow? The routing system is already deciding to route a HEAD request to a controller that knows how to deal with GET.
\Drupal\rest\RequestHandler::handle()is a very generic controller, that itself routes an incoming request to:@RestResourceplugin (depending on the route)It's that second point where things go wrong: Symfony already decided to map HEAD to GET, but now
RequestHandlerdoes$request->getMethod()which of course must return the actual HTTP method. We just need to do the same normalization that the (Symfony) routing system does.The only possible alternative is to change
ResourceBaseto also encode the PHP method to call in the route definition in some route option, and forRequestHandlerto then look at that. This comes with one enormous downside: not every\Drupal\rest\Plugin\ResourceInterfaceimplementation is required to use\Drupal\rest\Plugin\ResourceBase, so it will make the REST module's API even more dependent on that base class.Conclusion: the solution in the patch is IMO the best.
Comment #71
dawehnerAgain in other words, why can't the routing system itself handle all possible HEAD requests, when there is a GET route.
Comment #72
catchMoving this to 8.1.x since it's a straight bugfix.
Comment #73
dawehnerHere is my alternative idea.
Comment #74
alexpottThis issue is not looking particularly rtbc.
Comment #75
dawehnerSo yeah here is a more complete patch.
Comment #77
wim leersRetesting because #75 applies cleanly here.
Comment #79
wim leersI applied #75 and ran it locally. It fails, as I predicted in #70.
With #75,
RequestHandlertries to invoke ahead()method on the REST resource plugin, but that doesn't exist, so it fatals:This is why we will always need the first hunk of #66, no matter what other nice additions we make to our routes to better describe what they support. But again, the additions that #73/#75 make to make our routes be more self-descriptive are also rather pointless, because Symfony already does that automatically for all routes anyway. Symfony does that automatically, because that's what RFC2616 prescribes.
Hopefully the failures in #75 (that should appear in ~30 mins) will convince you, and help you arrive at the same conclusion as I had in #70 :)
Comment #80
dawehnerWell, I am still 100% convinced that we could implement a generic HEAD support, no matter how much text you write, its just though a little bit tougher than expected. Let's do that in a follow up.
Comment #82
wim leersSo, restoring the patch from #66. But since @catch moved this to 8.1.x in #72, there's now a patch for 8.1.x and another for 8.2.x. It's impossible to roll a patch that applies cleanly to both. (Because: tests have changed, a single REST settings config file vs REST Resource Config Entities…)
Follow-up created: #2775479: Try to remove the "map HEAD to GET" logic in \Drupal\rest\RequestHandler::handle().
I'll let @dawehner RTBC.
Comment #83
dawehnerThank you wim and sorry for trying to solve things on a more fundamental level.
Comment #84
wim leersPlease don't apologize, I wish that were possible — and maybe we'll still find a way in #2775479: Try to remove the "map HEAD to GET" logic in \Drupal\rest\RequestHandler::handle() :)
Comment #85
dawehnerComment #86
alexpottCommitted 7766139 and pushed to 8.2.x. Thanks!
Committed 8baec91 and pushed to 8.1.x. Thanks!
Comment #92
jwjoshuawalker commentedJust a heads up, not sure if the outcome will be Symfony implementing HEAD request support, but I have a valid use case now that I've brought to their attention.
https://github.com/symfony/symfony/issues/28743