#246 | rest_cacheable_response-2765959-246.patch | 51.59 KB | Wim Leers |
|
#246 | interdiff.txt | 2.42 KB | Wim Leers |
#244 | rest_cacheable_response-2765959-244.patch | 49.25 KB | Wim Leers |
|
#244 | interdiff.txt | 1.29 KB | Wim Leers |
#240 | rest_cacheable_response-2765959-240.patch | 48.02 KB | Wim Leers |
|
#240 | interdiff.txt | 835 bytes | Wim Leers |
#238 | rest_cacheable_response-2765959-238.patch | 48.02 KB | Wim Leers |
|
#238 | interdiff.txt | 2.64 KB | Wim Leers |
#235 | rest_cacheable_response-2765959-235.patch | 46.44 KB | Wim Leers |
|
#232 | rest_cacheable_response-2765959-232.patch | 45.58 KB | Wim Leers |
|
#231 | rest_cacheable_response-2765959-231.patch | 72.08 KB | Wim Leers |
|
#224 | rest_cacheable_response-2765959-224.patch | 77.62 KB | Wim Leers |
|
#224 | interdiff.txt | 11.95 KB | Wim Leers |
#222 | rest_cacheable_response-2765959-222.patch | 77.66 KB | Wim Leers |
|
#222 | interdiff.txt | 31.62 KB | Wim Leers |
#221 | rest_cacheable_response-2765959-221.patch | 73.79 KB | Wim Leers |
|
#221 | interdiff.txt | 1.93 KB | Wim Leers |
#219 | rest_cacheable_response-2765959-219.patch | 71.91 KB | Wim Leers |
|
#217 | rest_cacheable_response-2765959-217.patch | 72.05 KB | Wim Leers |
|
#217 | interdiff.txt | 6.82 KB | Wim Leers |
#207 | rest_cacheable_response-2765959-207.patch | 65.68 KB | Wim Leers |
|
#207 | interdiff.txt | 18.28 KB | Wim Leers |
#205 | rest_cacheable_response-2765959-205.patch | 65.75 KB | Wim Leers |
|
#205 | interdiff.txt | 4.58 KB | Wim Leers |
#202 | interdiff.txt | 2.42 KB | Wim Leers |
#202 | rest_cacheable_response-2765959-202.patch | 66.96 KB | Wim Leers |
|
#198 | rest_cacheable_response-2765959-198.patch | 67.19 KB | Wim Leers |
|
#198 | interdiff.txt | 2.73 KB | Wim Leers |
#195 | rest_cacheable_response-2765959-195.patch | 67.19 KB | Wim Leers |
|
#195 | interdiff.txt | 1.56 KB | Wim Leers |
#194 | rest_cacheable_response-2765959-194.patch | 66.13 KB | Wim Leers |
|
#194 | interdiff.txt | 2.03 KB | Wim Leers |
#193 | rest_cacheable_response-2765959-193.patch | 65.84 KB | Wim Leers |
|
#191 | rest_cacheable_response-2765959-191.patch | 65.18 KB | Wim Leers |
|
#191 | interdiff.txt | 1.64 KB | Wim Leers |
#188 | rest_cacheable_response-2765959-188.patch | 65.25 KB | Wim Leers |
|
#188 | rebase-interdiff.txt | 6.78 KB | Wim Leers |
#188 | interdiff.txt | 10.76 KB | Wim Leers |
#184 | rest_cacheable_response-2765959-184.patch | 59.5 KB | Wim Leers |
|
#184 | interdiff.txt | 2.73 KB | Wim Leers |
#183 | rest_cacheable_response-2765959-183.patch | 57.3 KB | Wim Leers |
|
#183 | interdiff.txt | 2.45 KB | Wim Leers |
#182 | rest_cacheable_response-2765959-182.patch | 56.69 KB | Wim Leers |
|
#182 | interdiff.txt | 1.84 KB | Wim Leers |
#181 | rest_cacheable_response-2765959-181.patch | 54.92 KB | Wim Leers |
|
#181 | interdiff.txt | 2.58 KB | Wim Leers |
#180 | rest_cacheable_response-2765959-180.patch | 52.84 KB | Wim Leers |
|
#180 | interdiff.txt | 3.87 KB | Wim Leers |
#179 | rest_cacheable_response-2765959-179.patch | 50.67 KB | Wim Leers |
|
#179 | interdiff.txt | 9.99 KB | Wim Leers |
#178 | rest_cacheable_response-2765959-178.patch | 44.5 KB | Wim Leers |
|
#178 | interdiff.txt | 4.16 KB | Wim Leers |
#177 | rest_cacheable_response-2765959-177.patch | 40.83 KB | Wim Leers |
|
#177 | interdiff.txt | 9.28 KB | Wim Leers |
#176 | rest_cacheable_response-2765959-176.patch | 33.9 KB | Wim Leers |
|
#176 | interdiff.txt | 5.83 KB | Wim Leers |
#171 | rest_cacheable_response-2765959-171.patch | 28.2 KB | Wim Leers |
|
#171 | interdiff.txt | 1.52 KB | Wim Leers |
#170 | rest_cacheable_response-2765959-170.patch | 29.65 KB | Wim Leers |
|
#170 | interdiff.txt | 1.93 KB | Wim Leers |
#169 | rest_cacheable_response-2765959-169.patch | 31.52 KB | Wim Leers |
|
#169 | interdiff.txt | 5.57 KB | Wim Leers |
#167 | rest_cacheable_response-2765959-167.patch | 30.97 KB | Wim Leers |
|
#167 | interdiff.txt | 868 bytes | Wim Leers |
#160 | rest_cacheable_response-2765959-160.patch | 31.03 KB | Wim Leers |
|
#160 | interdiff.txt | 21.87 KB | Wim Leers |
#158 | rest_cacheable_response-2765959-158.patch | 33.46 KB | Wim Leers |
|
#158 | interdiff.txt | 898 bytes | Wim Leers |
#155 | rest_cacheable_response-2765959-155.patch | 32.62 KB | Wim Leers |
|
#155 | interdiff.txt | 1.12 KB | Wim Leers |
#151 | rest_cacheable_response-2765959-151.patch | 31.99 KB | Wim Leers |
|
#151 | interdiff.txt | 1.97 KB | Wim Leers |
#149 | rest_cacheable_response-2765959-149.patch | 30.11 KB | Wim Leers |
|
#145 | rest_cacheable_response-2765959-145.patch | 34.81 KB | davidwbarratt |
|
#145 | inner-diff.txt | 720 bytes | davidwbarratt |
#142 | inner-diff.txt | 1.04 KB | davidwbarratt |
#142 | rest_cacheable_response-2765959-142.patch | 35.51 KB | davidwbarratt |
|
#141 | inner-diff.txt | 1.37 KB | davidwbarratt |
#141 | rest_cacheable_response-2765959-141.patch | 36.55 KB | davidwbarratt |
|
#139 | rest_cacheable_response-2765959-139.patch | 37.92 KB | davidwbarratt |
|
#139 | inner-diff.txt | 1.24 KB | davidwbarratt |
#137 | inner-diff.txt | 911 bytes | davidwbarratt |
#137 | rest_cacheable_response-2765959-137.patch | 39.16 KB | davidwbarratt |
|
#135 | inner-diff.txt | 932 bytes | davidwbarratt |
#135 | rest_cacheable_response-2765959-135.patch | 38.63 KB | davidwbarratt |
|
#133 | rest_cacheable_response-2765959-133.patch | 37.72 KB | davidwbarratt |
|
#131 | inner-diff.txt | 685 bytes | davidwbarratt |
#131 | rest_cacheable_response-2765959-131.patch | 39.5 KB | davidwbarratt |
|
#129 | rest_cacheable_response-2765959-129.patch | 39.5 KB | davidwbarratt |
|
#127 | rest_cacheable_response-2765959-127.patch | 42.06 KB | davidwbarratt |
|
#125 | inner-diff.txt | 3.31 KB | davidwbarratt |
#125 | rest_cacheable_response-2765959-125.patch | 42.15 KB | davidwbarratt |
|
#123 | inner-diff.txt | 1.02 KB | davidwbarratt |
#123 | rest_cacheable_response-2765959-123.patch | 41.86 KB | davidwbarratt |
|
#120 | inner-diff.txt | 702 bytes | davidwbarratt |
#120 | rest_cacheable_response-2765959-120.patch | 41.76 KB | davidwbarratt |
|
#117 | inner-diff.txt | 626 bytes | davidwbarratt |
#117 | rest_cacheable_response-2765959-117.patch | 41.75 KB | davidwbarratt |
|
#116 | inner-diff.txt | 22.59 KB | davidwbarratt |
#116 | rest_cacheable_response-2765959-116.patch | 41.76 KB | davidwbarratt |
|
#110 | inner-diff.txt | 52.2 KB | davidwbarratt |
#110 | rest_cacheable_response-2765959-109.patch | 57.69 KB | davidwbarratt |
|
#106 | inner-diff.txt | 812 bytes | davidwbarratt |
#106 | rest_cacheable_response-2765959-106.patch | 25.05 KB | davidwbarratt |
|
#104 | inner-diff.txt | 7.24 KB | davidwbarratt |
#104 | rest_cacheable_response-2765959-104.patch | 25.12 KB | davidwbarratt |
|
#99 | inner-diff.txt | 2.33 KB | davidwbarratt |
#99 | rest_cacheable_response-2765959-99.patch | 23.3 KB | davidwbarratt |
|
#97 | inner-diff.txt | 7.12 KB | davidwbarratt |
#97 | rest_cacheable_response-2765959-97.patch | 20.96 KB | davidwbarratt |
|
#94 | inner-diff.txt | 1.14 KB | davidwbarratt |
#94 | rest_cacheable_response-2765959-94.patch | 21.62 KB | davidwbarratt |
|
#92 | inner-diff.txt | 6.04 KB | davidwbarratt |
#92 | rest_cacheable_response-2765959-91.patch | 21.32 KB | davidwbarratt |
|
#91 | rest_cacheable_response-2765959-90.patch | 19.55 KB | davidwbarratt |
|
#76 | inner-diff.txt | 9.21 KB | davidwbarratt |
#76 | D8.1-rest_cacheable_response-2765959-69-76.patch | 22.61 KB | davidwbarratt |
|
#72 | inner-diff.txt | 1.32 KB | davidwbarratt |
#72 | D8.1-rest_cacheable_response-2765959-69-72.patch | 15.27 KB | davidwbarratt |
|
#67 | rest_cacheable_response-2765959-67.patch | 18.96 KB | davidwbarratt |
|
#67 | inner-diff.txt | 2.33 KB | davidwbarratt |
#62 | rest_cacheable_response-2765959-62.patch | 15.7 KB | davidwbarratt |
|
#62 | inner-diff.txt | 1.23 KB | davidwbarratt |
#60 | inner-diff.txt | 1.19 KB | davidwbarratt |
#60 | rest_cacheable_response-2765959-60.patch | 14.78 KB | davidwbarratt |
|
#54 | rest_cacheable_response-2765959-54.patch | 14.24 KB | davidwbarratt |
|
#55 | D8.2-rest_cacheable_response-2765959-54.patch | 11.17 KB | davidwbarratt |
|
#34 | rest_cacheable_response-2765959-34.patch | 11.45 KB | davidwbarratt |
|
#34 | inner-diff.txt | 472 bytes | davidwbarratt |
#30 | rest_cacheable_response-2765959-30.patch | 9.71 KB | davidwbarratt |
|
#30 | inner-diff.txt | 706 bytes | davidwbarratt |
#29 | inner-diff.txt | 4.88 KB | davidwbarratt |
#29 | rest_cacheable_response-2765959-29.patch | 9.71 KB | davidwbarratt |
|
#27 | inner-diff.txt | 1.35 KB | davidwbarratt |
#27 | rest_cacheable_response-2765959-27.patch | 6.19 KB | davidwbarratt |
|
#20 | rest_cacheable_response-2765959-20.patch | 6.13 KB | davidwbarratt |
|
#20 | inner-diff.txt | 1.77 KB | davidwbarratt |
#5 | rest_cacheable_response-2765959-5.patch | 2.25 KB | davidwbarratt |
|
#7 | D8.0-rest_cacheable_response-2765959-6.patch | 3.03 KB | davidwbarratt |
|
#5 | inner-diff.txt | 0 bytes | davidwbarratt |
#4 | D8.0-rest_cacheable_response-2765959-2-do-not-test.patch | 2.23 KB | davidwbarratt |
|
#2 | rest_cacheable_response-2765959-2.patch | 2.25 KB | davidwbarratt |
|
#6 | inner-diff.txt | 821 bytes | davidwbarratt |
#6 | rest_cacheable_response-2765959-6.patch | 3.05 KB | davidwbarratt |
|
#19 | inner-diff.txt | 7.42 KB | davidwbarratt |
#19 | rest_cacheable_response-2765959-19.patch | 4.36 KB | davidwbarratt |
|
#24 | inner-diff.txt | 2.31 KB | davidwbarratt |
#24 | rest_cacheable_response-2765959-24.patch | 6.19 KB | davidwbarratt |
|
#31 | D8.0-rest_cacheable_response-2765959-30.patch | 8.59 KB | davidwbarratt |
|
#33 | inner-diff.txt | 1.49 KB | davidwbarratt |
#33 | rest_cacheable_response-2765959-33.patch | 11.2 KB | davidwbarratt |
|
#35 | D8.0-rest_cacheable_response-2765959-34.patch | 10.29 KB | davidwbarratt |
|
#48 | inner-diff.txt | 2.67 KB | davidwbarratt |
#48 | rest_cacheable_response-2765959-48.patch | 12.16 KB | davidwbarratt |
|
#56 | inner-diff.txt | 435 bytes | davidwbarratt |
#56 | rest_cacheable_response-2765959-56.patch | 14.23 KB | davidwbarratt |
|
#64 | inner-diff.txt | 2.82 KB | davidwbarratt |
#64 | rest_cacheable_response-2765959-64.patch | 16.63 KB | davidwbarratt |
|
#71 | D8.1-rest_cacheable_response-2765959-69.patch | 14.14 KB | davidwbarratt |
|
#70 | D8.2-rest_cacheable_response-2765959-69.patch | 16.28 KB | davidwbarratt |
|
#69 | inner-diff.txt | 3.3 KB | davidwbarratt |
#69 | rest_cacheable_response-2765959-69.patch | 19.85 KB | davidwbarratt |
|
Comments
Comment #2
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #4
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedHere's a Drupal 8.0.x version of the patch.
Comment #5
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #6
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #7
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #8
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #9
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #11
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #12
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #13
Wim LeersThanks so much! Especially for your crystal-clear bug report, but also the patch! :) Much appreciated!
This is the wrong place to be making this change. The problem is that this is lacking cacheability metadata for the resource itself.
AFAICT this is repurposing the
4xx-response
cache tag for a different purpose, that just happens to work in the special case you have, but it won't work in every case.The root cause here is that the REST module was architected in a time where the cache system in D8 wasn't matured yet. So it uses the Symfony convention of
… which then also throws away cacheability metadata in the access check result.
The proper solution would be to not throw that exception in
EntityResource::get()
anymore, but instead send aCacheableResponse()
with a 403 status, and the cacheability metadata stored in$entity_access
:return (new CacheableResponse('', 403))->addCacheableDependency($entity_access);
Comment #14
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedI thought about doing that, as a 403 should really have the
node:123
tag or something like that, but I wasn't sure if there was a way to do that in the rest module or if every resource was going to have to be responsible for adding it. :/Comment #15
Wim LeersWell, yes, every REST resource whose access depends on modifiable data (like an entity) will have to return a 403/404 response with the necessary cacheability metadata, they should not use the Symfony exceptions. That is annoying (but is only because Symfony's HTTP kernel/response objects/etc design is frankly quite short-sighted), but necessary. It won't be necessary for many REST resources though: most don't have this advanced access checking logic. And for those that do, this would mean core would be providing the right example.
Comment #16
dawehnerWell, it feels like optimizing for 400 errors is kind of a weird one. If people want to, they can always bypass caching easily for those.
Comment #17
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedIt would be nice if we had exceptions that allowed you to add cachability metadata to them, but I guess what you're saying is the best solution for now.
I'll take a stab at it.
Comment #18
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedWhat should we do for 404's? Right now, we just get this uncached response:
Comment #19
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #20
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedAdd the clearing of the
4xx-response
tags back.Comment #22
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #24
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedAlways return the access result as an object.
Comment #25
Wim LeersThis patch looks great to me. It's only missing explicit tests for entity REST resources first resulting in a 404 or 403 and then after modifying the entity resulting in a 200.
Comment #27
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedResolving existing tests...
Comment #29
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #30
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedwhoops....
Comment #31
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedDrpual 8.0.x version
Comment #33
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedI realized there are times where there is no cachable metadata to return, so to make an easier DX; let's just change
Response
toCacheableResponse
. This will make responses cacheable by default (which seems to be the norm). It also makes existing REST resources backwards-compatible with the change. Lastly, it allows people to customize the cache policies, where before it was always not cachable.Comment #34
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedwhoops....
Comment #35
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedDrupal 8.0.x version (for anyone still using that version).
Comment #37
Wim LeersWhy are we changing these?
There's no cacheability metadata for exceptions.
These seem out of scope for the same reason as the first point.
This one also seems out of scope for the same reason as the first point.
i.e. the scope of this issue should be to explicitly support 403/404 responses for
EntityResource
responses having the appropriate cache tags. It should not change "responses for exceptions", because those don't carry cacheability metadata anyway.Comment #38
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedSee #33.
Basically, if you have a REST Resource where it's a 404, there is no cachable metadata because there is no resource. To make an easier DX, let's just allow the 404 to get cached (if the policies allow it). If not, it will be uncached as usual.
But I don't see any reason a dev needs do this:
when they could just do this:
which really should do the same thing.
Comment #39
Wim LeersThey DO NOT need to do
They need to do
Because you need a response object that implements
\Drupal\Core\Cache\CacheableResponseInterface
to be able to add cacheability metadata.can never do the same thing, because an exception does not carry cacheability metadata. It could, but it doesn't. And doing that would require Drupal developers to never use Symfony's
HttpException
classes, but only Drupal's "cacheability metadata-compatible exceptions". Which is not realistic.So, let's please focus only on the concrete problem, and not try to solve the generic problem. Because solving the generic problem would require 10 times as many changes. Feel free to create a new issue for that though, if you feel passionate about this.
Comment #40
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedWhere does
come from on a 404? There is no entity, what are we checking access against?
Comment #41
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedFrom core, take a look at
Drupal\dblog\Plugin\rest\resource\DBLogResource
Is there any cachable metadata in there?
Comment #42
dawehnerI still would like to question this issue with my comment #16
Comment #43
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedWe are just making REST resources behave just like HTML resources. In the HTML resources, 4xx errors are cachable (per the cachability policies in core). If someone wants to cache them (or not cache them) with this change, they can do that.
Without this change you are forced to manually modify the
Cache-Control
header and manually collect the cache tags. :(Comment #44
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedAlso, if both of you could take a look at
Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber
in core, you'll see that for Exceptions that return HTML, a cachable response is always returned. I think the same should happen for any format (HTML, JSON, XML, etc.). The patch in #34 makes it consistent regardless of the format.Comment #45
Wim LeersTrue, you can't have that in case of a 404. So I guess I'm saying I so far am only convinced by the 403 handling. Although even in that case you have authentication to deal with, so … I wonder to what extent it's even cacheable.
No, there isn't. And that's why that response cannot be cacheable safely(in the sense that it can never be guaranteed to be up to date if it were made cacheable, because there are no cache tags for DB log entries). Entities do have cache tags, so there we can do that.
Hm good point. @davidwbarratt said:
— except one of the big reasons we make 4xx HTML responses cacheable by PageCache is because search engine spiders/robots tend to cause a lot of requests. Having many requests to REST routes is less likely. Even less so because usually you need to be authenticated. And if you're authenticated, the Page Cache is no use anyway.@davidwbarratt: I'm guessing that in your use case, you're doing REST requests for anonymous users? (i.e. anonymous users can access those routes?)
@dawehner: I think that in general it'd be interesting to have explicit support for REST+PageCache for anonymous user use cases, and for REST+Dynamic Page Cache for authenticated user use cases.
Comment #46
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedWithout this change, if you do a 404 on a node route, you get this:
With the patch you get this:
Even without any entity at all, you do get cache tags, just like you would if you get an HTML version of the the page:
I'm not actually sure what's setting the html response to not be cached (it might just be something in my configuration). But regardless, in the first example, there isn't a way to cache it at all, in the second and third, you can modify the cache handling.
Since the
ParamConverter
throws theNotFoundException
, there isn't a place to change this toResourceResponse
(and I think it would be inappropriate to do so). However, it is imperative that there be a way to cache the response.Yes, our REST endpoints are publically accessible, and there is a high possibility that we could have a large number of 404 hits. For instance, if someone referenced an article from a page, and then that article was deleted, the reference to the existing article would still be there. Because of this, when an application tries to access the article, they would get a 404 response. Even if the initial page is cached, the request for the related article would not be.
As an example, the page could be hit 1,000,000 times. All of these would be cache hits except for the first request. However, because of the 404 reference, we would go from 1,000,000 cache hits on the article resource, to 0 cache hits on the article resource and 1,000,000 hits to the origin server. What wasn't an issue at all, becomes a major issue that can bring down an origin server just because an editor deletes an article and are unaware of the reference.
This has actually happened to us, in production, on Acquia (if you want to look it up). :)
Comment #47
dawehnerThank you for explaining a bit what is going on. Just to be clear, if someone is attacking your site though, you have a hard time in Drupal anyway as its trivial to bypass caching layers.
IMHO we should rename the issue title and focus the issue a bit more on GET requests, see some reasons below.
Its honestly a bit weird that we add cacheabiliy metadata for non GET/HEAD requests ... These access denied conditions vary by the submitted fields for example, so do we vary somewhere by the HTTP body? I don't see that. Yes, DELETE requests are theoretically cacheable, given they are idempotent, but that's a weird optimization strategy
Just reading that code ... we are throwing an exception and then assuming we know how to cache that. Do we know what varied in order to throw the exception? To do this right one would need for example some kind of cache context for the serialized value at least. I don't see that.
Comment #48
davidwbarratt CreditAttribution: davidwbarratt as a volunteer commentedComment #50
dawehnerI would totally agree in the case we would get the cacheability metadata right, but for example for POST requests one would have to vary by the POST data, but we don't yet.
Given that IMHO we should either implement it correctly, or not at all.
Comment #52
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedThat's my point though, we already do not cache POST requests. This patch does not change that at. It does enable a developer to cache them if they want to do that properly. Without this patch there is no way to do that (without overriding the plugin I guess).
Comment #53
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedSo #34 has a bug where if a response throws a
NotAcceptableHttpException
then it returns these headers:Then if you go back to a response that should return a 200, you get the same cached response back as the error.
I believe this is because in that patch the Cache-Context is assumed to be
user.permissions
.It looks like what I have in #48 is correct. Since we are not making any assumptions on the Cache-Contexts, then we shouldn't have any Cache-Contexts in the response. Without any cache contexts, Drupal marks the request as uncachable.
Comment #54
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedRe-Roll.
Comment #55
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #56
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #60
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #62
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #64
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #65
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #67
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #69
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #70
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #71
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #72
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #73
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #76
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #77
dawehnerMh, so by what does this cacheable response varies at that point? All those responses vary by nothing, which, can't be right.
Don't we ideally have to check access of all fields and then combine that? In general this also feels like an API break to move away from the exceptions into directly returning the response.
Comment #78
dawehnerComment #79
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedurl
) where a 405 would beurl headers:status
)? I think the real issue is, is that you should not be using the HTTP Exceptions unless there is no cachable metadata (see #39)Comment #80
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedPerhaps we should go through the Cache Contexts that are used on html requests and apply the ones that would apply on non-html request (i.e. I don't think
theme
would apply).Comment #81
dawehnerWell, this wasn't about the specific message. The problem is that when you want to cache something you need to know all factors by which this request could vary. Given that, we need to know all access result objects for every field access. You can beside of that still show just the error message of the first failed field.
Well, you know, throwing those exceptions are part of our API, and they have been longer than any render caching. If code cannot longer rely on being executed, I am quite convinced we add way more bugs, than we try to solve here.
Comment #82
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedIf you take a look at DBLogResource::get() what should the cache contexts for the response (or the exceptions) be? I suppose it could/should always be varied by
url.path
?Comment #83
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedI think it's ok to throw the exceptions when you don't have any cachable metadata, but that shouldn't prevent the request from being cached (it doesn't in html format, so why would it in any other format?)
Comment #84
dawehnerYeah, this would vary by url.path, as well as this would require something like a cache tag for watchdog entries. As you see, this all potential causes some BC issues.
Comment #85
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedWatchdog entries don't have cache tags because they wouldn't be cleared out when you create, update, or delete an entry. So even if we added one, it wouldn't help since it's not an entity.
I could take a stab at adding contexts to the exceptions, I mean they will be pretty broad. But right now, like I've said, you can throw an exception in an HTML response and you get a cachable response (with default cache contexts and cache tags).
As far as the BC break. I think this is a bug, it's not a feature request. We are standardizing the API to have a consistent response between the html format and other formats. I think it was a complete oversight that these responses were not cachable in the first place (see #13).
Comment #86
dawehnerBC breaks should still not be introduced as part of fixing bugs, as we basically replace one bug fix by another :)
Comment #87
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedFine... so is there a way to cache 404's without a BC break? I should say there are two different instances of a 404, there's a route not found exception as well as a param not converted exception.
Since everything is returned as a standard Response object, I don't think there's a way to do it, other than to introduce another middleware I guess?
Comment #88
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedI guess what we could do, is create an Experimental module that would override the exception handler?
Comment #89
dawehnerI think making the changes just to the ResourceHandler would be way more safe, than changing the generic exception subscriber, which might be used by a lot of other code. Does that make sense?
Comment #90
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedWell, that's what I started doing in #6, but by #34 I figured out that that wasn't going to work. I explained in #46.
The problem is, is that if you pass a non-existant route, or a route with a bad paramter, the routing system throws an exception before it even gets to the REST module.
For instance, if you try to hit http://www.example.com/node/16354351?_format=json then the ParamConverterManager::convert() throws a ParamNotConvertedException.
If this is in html format, it's a cachable response, if it's in any other format, the response is not cachable. I think it would be best if it was cachable regardless of the format.
Let me know if you can think of a way to do this in the best BC possible way, #69 is the best I could come up with.
Alternatively, the only way I can think to do this is by making a module that overrides the exception handler as well as the REST RequstHandler. I think that's monkey-patching, but it would work and wouldn't break BC.
Comment #91
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedReroll.
Comment #92
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedAdding support for 401 which is for some reason missing. :(
Comment #94
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedResolve the error(s).
Comment #97
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedRemove code leakage and resolve tests.
Comment #99
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedI saw this and I thought it was code leakage from somewhere, but it looks like it resolves one of the tests... it sure looks funky though. Not sure how else to resolve it.
Comment #100
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedOh I see... because we are no longer throwing an exception, we have to catch the 403 response, rather than the 403 exception.
Comment #102
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #104
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedMoving the cache context to a response subscriber and ensuring that cachable metadata is not overridden when returning a response rather than an exception.
Comment #106
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedAsserting that there is no response. Since we are using the existing response object rather than throwing an exception, the message cannot be passed along and serialized. :(
The choice is either to have cachable metadata passed along, or to have the message passed. I'm going to opt for the cachable metadata.
This is making me realize that more and more we need a
CachableHttpException
. It would be nice to be able to throw an exception and be able to add cacheable metadata to that exception like you can with aCacheableResponse
. That would fix this problem because you could have both the message as well as the metadata.However, that is a lot of changes and while I suppose it could be BC because we could support cachable and non-cachable exceptions (like we do for responses) it just feels like a lot of architectural changes. Then again, if that is overall a better solution, then that's probably the direction we should go.
Comment #107
Wim LeersNice progress here!
Why change this? Because the 4xx response can also happen for routes other than the canonical one?
Why is this necessary? Why is the existing
\Drupal\Core\EventSubscriber\AuthenticationSubscriber::onExceptionSendChallenge()
insufficient?I have suspicions, but the documentation on this method doesn't explain it at all. Nor is there explicit test coverage being added.
These changes are effectively duplicating
\Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber
.We should consolidate them, not add more such implementations.
I'd be fine with moving
\Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber
intocore/lib/Drupal/Core/EventSubscriber
and deprecating the original one, and thisExceptionJsonSubscriber
.Hm… this is going to cache a separate response for every possible value of the
Authorization
header. That seems problematic too.Also, this points to missing test coverage for cached 4xx responses to prove me wrong :)
Ah! This at least partially explains the changes in
AuthenticationSubscriber
.Related: #2808233: REST 403 responses don't tell the user *why* access is not granted: requires deep Drupal understanding to figure out.
Finally: why this approach? Why not subclass
AccessDeniedHttpException
so that the exception can also carry cacheability metadata?That'd be a far less invasive change.
Why these changes? PATCH/POST/DELETE responses are not cacheable by definition anyway.
Why delete this trait?
Interesting… this would definitely need explicit test coverage in
\Drupal\Tests\rest\Kernel\RequestHandlerTest()
. And it probably needs to happen in a separate issue.This is already being fixed in #2813853: RequestHandler has its own error handling rather than leaving it to KernelEvents::EXCEPTION event subscribers.
EDIT: cross-posted with #106.
+1. But please make it
CacheableHttpExceptionInterface
+CacheableHttpExceptionTrait
. Then you can simply do:Comment #108
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedThanks for the feedback!
\Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber
requires the seralization service which is only available in the module. If we move it we'd also have to enable the module. I could make a more generic class and extend both of them from the same one, then just the constructors and::setEventResponse
would be different. Is that better?user.permission
because I was accidentally overriding the cachable metadata. I'll see what happens with that context rather than the header context.Comment #110
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #111
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #114
Wim Leers#108.3: yes that'd be better.
#108.6: no, caching for POST/PATCH/DELETE never makes sense. They must result in modifications to the stored data, or at least attempted modifications. Which means their responses can never be cached.
Retesting #110, because CI error.
So much better!
s/cachable/cacheability/
Why modify these? They don't contain cacheability metadata.
Perhaps you were trying to update all code throwing Symfony HTTP exceptions? I think that's not necessary; just updating the ones that do have cacheability metadata is sufficient.
Comment #116
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedBased on #114, I've simplified the patch by removing any use of the new Cacheable Exceptions when the request is for certain not a GET request. When the request could be a GET request, a Cacheable Exception should be used, even if we are not adding any cacheable metadata. Making the exception cachable allows us to cache the request under certain conditions, if it's not a cacheable exception, then it will never be cached.
For this patch, I modified all of the places I could find that an
HttpException
could be thrown on a GET request in core. There might be more places, but I think I've covered most of them.I think this is the cleanest solution so far. It resolves the problem and it doesn't introduce new issues (from what I can see). It also allows developers to add cacheable metadata to exceptions, which I think is an awesome new feature.
Probably should still add specific tests for this issue, not sure where they should go.
Also, is this a bc break? I don't think it is, but if it is, how should we resolve it? I guess we could add a config option? what would it do?
Comment #117
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedResolving a Fatal PHP Error. :)
Comment #120
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedResolve another PHP Fatal Error.
Comment #121
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #123
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #125
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedResponses need to be prepared from data in Request (how was this ever working??)
Comment #127
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedTo preserve backwards comparability (and resolve these tests). We will only send a
CacheableResponse
when aCacheableHttpException
is thrown. If it is not, then a standardResponse
will be returned.Comment #129
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedReroll
Comment #131
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #133
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #135
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #137
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #139
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #141
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #142
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #145
davidwbarratt CreditAttribution: davidwbarratt at Golf Channel commentedComment #147
Wim Leers#116: great work!
This is because the last line here is failing:
… which means that 403 it's asserting a few lines earlier is still being served. Despite that
setUpAuthorization()
line granting an additional permission to the tested role (anonymous
in this case). This probably means that the cached response does not have theuser.role:anonymous
cache context, unless this patch is of course introducing some other bug.Hopefully this helps getting the patch to a point where it passes all tests :)
Comment #149
Wim LeersI want to get this issue closed: either get it committed, or close it. This issue has been dragging on for too long already. So I went over this entire issue again. I read the IS, and every comment.
I do want to call out that since this issue started, and since the last patches that were posted, a lot has changed. Drupal 8 now has:
vastly expanded test coverage — for example: #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method
exception handling has been vastly improved (simpler, more robust, far more test coverage) — for example: #2739617: Make it easier to write on4xx() exception subscribers
vastly improved error responses — #2808233: REST 403 responses don't tell the user *why* access is not granted: requires deep Drupal understanding to figure out
All those changes do impact this issue.
#16: I agree that caching 4xx responses for REST is questionable, given that 95% of sites require authentication to access REST resources. However:
(I wrote something similar at the very bottom of #45.)
#46: thanks for providing an actual real-world use case for supporting anonymous, cacheable 4xx REST GET responses (with cache tags). But, as #47 says, if this an actual attack, and not merely a massive amount of traffic, then it's easy to bypass this cache anyway: just perform requests to Drupal with a querystring that contains the current UNIX timestamp, or a random value, and you'll bypass all caches.
#47: fully agreed that this issue should only handle
GET
requests/responses.#48 + #52: it doesn't matter that Page Cache will only cache
GET
andHEAD
responses (and so will Varnish or any other HTTP cache btw). It never makes sense to cache responses toPOST
,PATCH
orDELETE
requests.#53 + #79:
This is wrong. If there are no cache contexts, then Drupal concludes there are no contexts by which to vary. In other words: there's only one variation of the response for this request. If this response ends up not being cached, that's then happening due to other reasons, it's not happening because there are no cache contexts.
If you don't believe me, read
\Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber::shouldCacheResponse()
:There's no logic there for "no cache contexts are set, so then don't cache". Your response is not being cached due to other reasons.
#90: thanks for providing an overview of what you tried, and what did and did not work. It is very very helpful to be able to read your reasoning :)
I see that I've been fairly active in reviewing/responding from #107 onward. But then, as of #145, it looks like @davidwbarratt gave up. Understandable!
I think that the patch has been moving in the right direction in principle/in general.
But I also think that we need #2827797: ResourceResponse(Subscriber) + Dynamic Page Cache: making authenticated ResourceResponses significantly faster to be fixed before we can do this. Otherwise we still don't know if REST + Dynamic Page Cache + 4xx responses is actually working as intended.We do not need to wait for #2827797 to be fixed: that's merely improving how REST + Dynamic Page Cache work; it's making Dynamic Page Cache able to cache REST's responses more effectively.Before diving deep into a patch review, let's see how much of this patch is passing tests. A lot has changed, many things which simplify this patch; many of the test failures I see in #145 are related to things that have been improved since then.
So, I rebased this patch on top of HEAD. Lots of details have changed. Several changes are no longer necessary. And it seems that many if not most of the test failures in #145 have disappeared :)
Comment #151
Wim Leers20 of the 41 test failures report this:
20 of the 41 test failures report this:
Both of them happening at line 363 in
EntityResourceTestBase
. Both have the same root cause.This is because the response is actually a Page Cache HIT, instead of a Page Cache MISS. Why? Because the changes made in this patch cause the previous 4xx response to be cached and served!
In other words: this is causing incorrect 4xx responses, because the response is cached when it shouldn't be. Why is that? Because this request is attempting to use a disallowed authentication provider, yet Page Cache is still trying to cache it.
\Drupal\basic_auth\PageCache\DisallowBasicAuthRequests
prevents this from happening forbasic_auth
and\Drupal\Core\PageCache\RequestPolicy\NoSessionOpen
prevents this from happening forcookie
. So why is this not happening here? Because the REST test authentication providers do not yet have a page cache request policy denying them from being cached!Simple fix :)
Comment #152
Wim LeersWe still need to update the IS, but here's already an improved title. The current title keeps confusing me.
Comment #154
Wim LeersThe remaining test failures are for testing
GET
on theUser
entity type, only when using thehal_json
format, with the following failures:UserHalJsonBasicAuthTest
:Failed asserting that 401 is identical to 403.
(403 expected, 401 instead) on line 379 ofEntityResourceTestBase
, due to a Dynamic Page Cache hit that shouldn't be a Dynamic Page Cache hit. Most notably, there are no cache contexts associated with that cached 401 response!UserHalJsonAnonTest
:Failed asserting that 403 is identical to 200.
(200 expected, 403 instead) on line 389 ofEntityResourceTestBase
. This is neither a Page Cache hit nor a Dynamic Page Cache hit.UserHalJsonCookieTest
:Failed asserting that 403 is identical to 200.
(200 expected, 403 instead) on line 389 ofEntityResourceTestBase
, due to a Dynamic Page Cache hit that shouldn't be a Dynamic Page Cache hit. Most notably, there are no cache contexts associated with that cached 403 response!The question is of course:
. Why just this entity type, and why only in thehal_json
format?Investigating…
Comment #155
Wim LeersRoot cause for failing
UserHalJsonAnonTest
found: missing cache context in one of the cases in\Drupal\user\UserAccessControlHandler::checkAccess()
. Which resulted in zero cache contexts associated with the access denied exception, and hence inappropriate caching.Of course, that begs the question: why did
UserJsonAnonTest
not also fail?This took some further digging. Turns out the root cause is
\Drupal\Core\EventSubscriber\ExceptionJsonSubscriber::on4xx()
. This runs with priority -75. So thejson
format's 4xx responses are handled by this one.Contrast this with
\Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber::on4xx()
, which handles all serialization formats. It also has priority -75. But because it happens to be ordered before the aforementioned one despite the identical weight, this one did not handle thejson
format. But it did handle thehal_json
format.In other words…
UserJson*Test
should also have failed. But it didn't. So let's first show that we can make things fail consistently, for allUser
tests. Then we can confirm that the root cause (\Drupal\user\UserAccessControlHandler::checkAccess()
) in fact did apply in all cases.Comment #156
Wim LeersI'm awaiting the test result of #155. If all goes well, then we'll have 6 failures:
UserHalJsonAnonTest
UserHalJsonBasicAuthTest
UserHalJsonCookieTest
UserJsonAnonTest
UserJsonBasicAuthTest
UserJsonCookieTest
I will post the fix and next steps as soon as #155 indeed comes back with 6 failures. Please don't work on this issue in the mean time. It'll make this issue even harder to follow.
Comment #158
Wim LeersAlright, #155 had the 6 expected failures. Now we're in a consistent state. So now let's apply the bugfix I mentioned at the beginning of #155.
Comment #159
Wim Leers#158 should come back green. Time to do a proper review.
This, repeated for every single of these new classes… that makes for a fairly lengthy docblock. But I think it's warranted, because without this, it may end up confusing users.
(There are a bunch of nits to be fixed in all of these though.)
However, it's rather strange to see this use
CacheableResponseTrait
for an exception. So let's at least update the docs for that trait. Because it does make sense to use this trait here.I think this belongs in a separate issue.
This needs explicit test coverage of its own and the update to
EntityUnitTest::testPostSave()
does provide that.But what is missing here, is proof that we really need this. i.e. what's missing, is test coverage showing that a certain scenario fails unless we make these changes.
#108.1 said: … but the recently added test coverage for
Item
at #2843752: EntityResource: Provide comprehensive test coverage for Item entity (which is a content entity type without a UUID field) still passes without this AFAICT. So that suggest we don't need this.A response is being sent for an exception. The response must inherit the exception's cacheability metadata.
This is not associating any cacheability metadata. Which means it's unsafe to make this exception cacheable.
This should be done in a separate issue, with explicit test coverage.
We can prevent this duplication, by first creating the non-cacheable exception, then conditionally converting it to a cacheable one.
We should only have to add
$entity_access
as a cacheable dependency.If
$entity
affects theAccessResult
, then it's up to the access checking logic to add$entity
as a cacheable dependency to$entity_access
.Comment #160
Wim LeersThis addresses #159.1 + #159.2. This:
CacheableHttp*Exception
classesCacheableResponseInterface
point toCacheableHttpExceptionInterface
and vice versa, to clearly establish their link by similarityCacheableResponseTrait
CacheableHttpExceptionInterface
to not reuseCacheableResponseInterface
— reusing that interface means that all cacheable HTTP exceptions implement that interface, which could lead to if-tests that jump to the wrong conclusions and hence treat exceptions as responses!This has zero functional changes compared to #158, and should therefore have identical test results.
Comment #161
tstoecklerWhy is a failed parameter conversion cacheable? Seems like if I have 3 nodes and then visit /node/4 anonymously and then create another one, the 404 should not be cached? What am I missing?
Comment #162
Wim Leers#161: I agree, see #159.5. See @davidwbarratt's earlier comments wrt
ParamConversionEnhancer
, he's the one who wrote this. I intend to remove that hunk for sure.Comment #163
tstoecklerAhh sorry, missed that. Thanks for the confirmation!
Comment #164
davidwbarratt CreditAttribution: davidwbarratt commentedA
ParamConversionEnhancer
exception is a cachable exception in HTML:I think the API should match this behaviour.
Let's say you had 1,000/requests/minute going to
/node/2
and then you deleted it. At that moment, you'd go from having no requests for that node (since it's cached) to 1,000/requests/minute and there would be no way to stop it. :(Also, if you hadn't yet created
/node/4
that should be a cachable 404 until you create it, at which point it should be purged from the cache (which is exactly what happens with HTML).I don't really care the method in which we get to this, but I think public 404's, regardless of route or format, should be cacheable.
Comment #167
Wim LeersAhhh, finally! After endless DrupalCI infra problems, we now have the test results for both #158 and #160! They're both green, as expected :)
This then addresses #159.7.
@davidwbarratt: welcome back! I hope you're glad to see that your work has not been in vain! I'm currently working to address my review points in #159. To address #159.5 and your explanation in #164, I will simply remove your changes in a reroll, and observe what failures it causes. Then we'll take it from there. I'm not per se opposed to it, but we need to make sure that it's carefully tested, so that we don't accidentally break other things, or accidentally introduce security problems. I'm sure you agree with that.
Comment #169
Wim LeersAs predicted, #167 is green.
This then performs the next step: fixing #159.4 (fixing comments) and #159.6 (reducing duplication/brittleness in
BasicAuth
changes). These should be non-functional changes, so the patch should still come back green.Once this patch comes back green, we'll finally be able to address #159.3 (
Entity
changes) and #159.5 (ParamConversionEnhancer
changes). I will remove both of those changes, one-by-one, so we can observe the test failures (if any). Ideally, we'd be able to move those changes to separate issues.Comment #170
Wim LeersStill green, great.
Let's observe what happens if we revert #159.3.
Comment #171
Wim Leers#170's test run is still in progress, but all HAL and most REST tests already came back green. So I'm pretty sure #170 will come back green.
So now let's observe what happens if we revert #159.5 as well.
Comment #172
dawehnerHere is maybe a naive question. On block access (
\Drupal\block\BlockRepository::getVisibleBlocksPerRegion
) we need to take into account every possible combination of access to determine the cacheability metadata. Doesn't that mean we have to do the same for any access checking? I guess throwing exceptions is conceptually wrong (Note: In d7 we essentially did the same withdrupal_access_denied()
), and this issue doesn't cause more problems, given we do the same for normal routing already.Comment #173
tstoecklerRe #164: Thanks for that list, that reminded of the
4xx-response
which does "fix" exactly what I thought would break in #161 because it is automatically cleared on every entity save. So that somewhat answers question, although I can't really comment on the patch as a whole, that's a bit above my head.Comment #174
Wim LeersSo now we reverted #159.3 (in #170) and #159.5 (in #171). My review in #159 is now fully addressed!
However, @davidwbarratt added them to ensure we get cacheable 404 responses. We don't know if 404 responses are still cacheable after removing those hunks, because… @davidwbarratt had not yet added test coverage. That's also why I think it shouldn't surprise anybody that #170 and #171 came back green.
So, let's work on adding that test coverage. If we then notice that 404 responses that we'd like to be cached are not cached, we can still choose to restore those hunks. Or we can choose to defer that to a follow-up — this issue would then be responsible for
EntityResourceTestBase
If some of those 404 responses then are not cacheable, we would know about it, and it'd be much simpler to implement #159.3+#159.5 in a follow-up issue.
Finally, I'd say this involves a security risk, because missing cache contexts can cause information disclosure. But in this case, that's not true. If you get a 4xx response that you should not get, you're just being told the reason why you're being denied access. None of that is sensitive information. The worst that can happen, is that you're getting an incorrect 4xx response, and that it hence is worsening the DX. However, when such a case is encountered, we should simply add test coverage to
EntityResourceTestBase
and fix it. And the massive amount of test coverage this needs to add per the above would already be a strong guarantee that it's correct.This test coverage is what I've been working on today. Many tests are passing, but not yet all. I want to get to a better place before posting a patch here. Stay tuned — definitely expect a patch tomorrow :)
Comment #175
Wim LeersPer #174.
Comment #176
Wim LeersSo! Time to get that test coverage going! This took a lot of work. But it will result in an update for all test coverage added by #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method, and hence all that REST test coverage the all also asserting all cacheability aspects. This is hugely important for the future.
To get started: let's expand
ResourceTestBase::assertResource(Error)Response()
: let's update it to test all four things mentioned in #174:We'll let this by default assert that a response doesn't have a
X-Drupal-Cache-Tags
header, nor aX-Drupal-Cache-Contexts
header, nor aX-Drupal-Cache
header nor aX-Drupal-Dynamic-Cache
header. In other words: by default, all REST reponse assertions will check that the responses are not cacheable at all.As you'll see in the test results, this will already allow the
testPost()
,testPatch()
andtestDelete()
test coverage to mostly work. That makes sense, because POST/PATCH/DELETE are "unsafe" HTTP methods.Comment #177
Wim LeerstestGet()
forbasic_auth
andcookie
are now failing on the assertions inassertResponseWhenMissingAuthentication()
, because we need to update those. In case of:cookie
, we're just seeing the 403 response thrown by entity access denying us access inEntityResource::get()
, and so we need to expect that that 403 response is now cacheablebasic_auth
, we have a similar problem as forcookie
… but I realized that in fact it doesn't make sense to inherit the cacheability metadata of the 403 when we convert that 403 to a 401. In fact, it makes total sense to simply letbasic_auth
always generate a cacheable 401. Detailed explanation in a code comment in\Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
. Lots of explicit test coverage for various situations thanks to the REST test coverage.Comment #178
Wim LeersNow e.g.
NodeJsonBasicAuthTest
andNodeJsonCookieTest
are failing a bit further: it's time we update a whole bunch of theassertResource(Error)Response()
assertions, because the default assumption (as explained in #176) that those responses are not cacheable at all is not true for responses to GET requests, of course.As you can see in the interdiff, we did already have test coverage for cacheability of a successful 200 response. But that was the only test coverage we had for this. We're now changing that to have all received responses be tested for their cacheability!
Comment #179
Wim LeersYay, the REST
testGet()
test coverage forNode
is passing again! :)But … turns out that some entity types' access control handlers deviate from what's true in most cases: some have the cacheability depend on the entity in question (for example: access is granted only if the entity is published, or not locked), which then of course means that that entity's cache tag is present on the entity access result's cacheability metadata, which then is also present in the access result.
Therefore we add a new
\Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::getExpectedUnauthorizedAccessCacheability()
method, and have those entity types that need it override that default implementation in their test base class.Comment #180
Wim LeersNow almost all
testGet()
tests are passing!In fact, quite a lot is passing completely. Except for
testDelete()
, many of those seem to be failing at the same point.Comment #181
Wim LeersYay, lots of green.
Now let's update the
User
-specific edge case test coverage in\Drupal\Tests\rest\Functional\EntityResource\User\UserResourceTestBase::testPatchDxForSecuritySensitiveBaseFields()
.Comment #182
Wim Leers#2827797: ResourceResponse(Subscriber) + Dynamic Page Cache: making authenticated ResourceResponses significantly faster contains a fix for
\Drupal\system\EventSubscriber\AdminRouteSubscriber
that we also need here: otherwise theShortcut
,MenuLinkContent
andBlock
entities are treated differently by Dynamic Page Cache, because their paths begin with the prefix/admin
. And that causes those to behave differently than the other entity types.Comment #183
Wim LeersSome particularly nasty work-arounds are necessary for the strangeness going on in
\Drupal\block\BlockAccessControlHandler::mergeCacheabilityFromConditions()()
— well, really, in the context system.Comment #184
Wim LeersAnd then finally, an oversight in the cacheability metadata of REST responses: the
config:rest.settings
cache tag. This should have been added to the cacheability metadata in #2664780: Remove REST's resource- and verb-specific permissions for EntityResource, but provide BC and document why it's necessary for other resources, but it wasn't, because REST test coverage was not yet asserting cacheability of all responses. As of this patch we are, and so now we are seeing that this is a problem… at least in case of theBlock
entity.Another WTF for developers fixed/avoided!
If all goes well, this patch should then finally be green!
We now have comprehensive REST response cacheability test coverage! Whew.
Comment #185
Wim LeersIf #176 through #184 doesn't convince you this is major, then I don't know what will.
Comment #186
davidwbarratt CreditAttribution: davidwbarratt commentedYAY! Thank you @wim-leers!
Comment #187
Wim LeersPer #182.
Comment #188
Wim Leers#2765959: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage was committed, here's a reroll per #182.
Changes:
Drupal\Core\EventSubscriber\DefaultExceptionSubscriber
toDrupal\Core\EventSubscriber\FinalExceptionSubscriber
. The actual diff remained the same though, it just needed to be applied to a different file.rebase-interdiff.txt
._admin_route
-related changes …\Drupal\system\EventSubscriber\AdminRouteSubscriber
. However, a consequence is that this then requires the same temporary "if-else" work-around that existed in a few places in #2827797 to be introduced in many places in this patch (from 2 to 8).interdiff.txt
.Comment #189
Wim LeersTo be clear: #188 could be massively simplified if either #2874938: AdminRouteSubscriber must only mark HTML routes as administrative or #2877528: Change Dynamic Page Cache's response policy from "deny admin routes" to "deny html admin routes" land, or both. Then most of the changes in ##18's interdiff could be removed. Patch size would shrink roughly 8 KB.
Comment #191
Wim LeersI made a small mistake in #188 when splitting this up in the two interdiffs. Easy fix fortunately :)
Comment #192
tedbowNeeds reroll
Comment #193
Wim LeersRerolled. Conflicted with #2877778: Harden test EntityResource + Dynamic Page Cache test coverage, which hardened some test coverage that this patch moves around. Had to update this patch to move the updated code around instead. Effectively a straight rebase.
I'll work on an updated issue summary in the coming days.
Comment #194
Wim LeersThe hardening that #2877778: Harden test EntityResource + Dynamic Page Cache test coverage did worked well, but this patch breaks the assumptions that were made. The assumption that was made was that only 200 responses would ever be cached. Therefore, it assumed 2 cache items in the
cache_dynamic_page_cache
DB table: a cache redirect, and the actual cached response.However, this issue changes that: this issue makes 4xx responses cacheable too. Therefore it is possible to have >2 cache items: there may be cached 4xx responses.
So let's update the patch to check that:
That still maintains the spirit of #2877778: Harden test EntityResource + Dynamic Page Cache test coverage.
Comment #195
Wim LeersIn the mean time, #2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX also landed, which added additional test coverage. It also was asserting a 200 response. In this patch, we're making it required to check (Dynamic) Page Cache HIT/MISS and cache tags + contexts. So, let's update the assertion it introduced.
Comment #198
Wim LeersFixing coding standards violations in #195, using testbot's
codesniffer_fixes.patch
.Comment #199
Wim LeersIS rewritten. CR created: https://www.drupal.org/node/2884481.
This is ready for final review.
Comment #200
Wim LeersBetter title.
Comment #201
tedbow#159.2
So yes it seems weird to use CacheableResponseTrait in exceptions. The documenation was updated and it was noted so if no one else has problem with that I won't hold it up.
I guess it we can't rename this trait?
Comments for this interface still refer to responses though this is an interface for expections. Probably just copy/pass error from CacheableResponseInterface
In this if/else we make the same exact call to assertSame() in both cases.
Not sure what the intention was but that can't be right 😜
Comment #202
Wim LeersRE #159.2: No, we can't rename this trait. That'd be a BC break. What we can do is this:
Comment #203
dawehnerI know its status quo already, but its a bit sad that the cache component also deal with http caching. It feels like a better approach would separate that, but nevermind, just throwing out my thought here.
I'm sorry, but at least for me it never makes sense to cache a 500. This feels like unnecessary code complexity to be honest.
Maybe a naive question: The message changes depending on the system.site configuration. Does this mean it should be part of the cache tags?
What about simply just calculating the false variable of assertResourceErrorResponse. Then we could basically get rid of the if/else and probably replace it by an easy to read
?:
Comment #204
Wim Leers#203: thanks!
Comment #205
Wim Leers#203:
core/lib/Drupal/Core/Cache/Exception/
tocore/lib/Drupal/Core/Http/Exception/
? I think that would make sense!EntityResourceTestBase
, there are similar if/else statements where the if-case contains a lot more logic. Having consistent if/else statements makes it easy/low-risk to clean this up when either of those 2 issues land.Addressed points 2 and 3. I disagree on point 4. I'm happy to address point 1 if @dawehner likes my suggestion :)
Comment #206
dawehner... I was really mostly loudly thinking load. I'm glad someone else has the same opinion :)
Fair, I don't worry much to be honest :)
Comment #207
Wim Leers:)
Done!
BTW, what do you think about the proposal in #202?
Comment #208
dawehnerI'm wondering whether having simply a better name would be a better approach. This would then be in total 2 traits (the existing one + the better name) instead of 3, which is your proposal in #202.
Comment #210
Wim LeersThat could work too. So then what do you think about what I wrote in #202:
CacheabilityCarryingObjectTrait
?Comment #211
tedbowI would be in favor of @dawehner's idea in #208 adding just CacheabilityCarryingObjectTrait and not CacheableExceptionTrait
and then maybe deprecating CacheableResponseTrait because it could be replaced by CacheabilityCarryingObjectTrait
Comment #212
Wim Leers#211: Yep, we'd then indeed deprecate
CacheableResponseTrait
. Do you approve of the name?If @dawehner can confirm he also likes the name, I'll get it done.
Comment #213
dawehnerWFM
Comment #215
Wim LeersFirst, let's get this stable. Then, I'll implement #211, because it has both @tedbow's and @dawehner's approval.
Comment #216
Wim LeersThis change in #205 is what caused the test failures. Because it adds the
languages:language_interface
cache context, since thesystem.site
config may vary by interface translation.Working on a fix.
Comment #217
Wim LeersThe thing #216 didn't explain yet, is that it only does that for
ConfigurableLanguage*BasicAuthTest
andContentLanguageSettings*BasicAuthTest
because they both install thelanguage
module. Annoying! But correct.The solution is fortunately not very hard: an alternative trait for
BasicAuthResourceTestTrait
for those tests that do install thelanguage
module.This should be green again, but I still need to implement #211.
Comment #219
Wim LeersTime to continue here again. Finally.
This conflicted pretty heavily with #2869387: Subclasses of ResourceTestBase for non-entity resources are required to add pointless code. First rebasing.
Comment #221
Wim LeersThose 12 failures only exist because more REST test coverage has been committed since June. Two of those newly committed pieces of test coverage (
Media
andBlockContent
) obviously associate cacheability metadata with their "access denied" access result.This should be green again.
Comment #222
Wim LeersImplemented #215 (which is #202 + #208 + #210 + #211 + #212 + #213). Except I've simplified it even further compared to what @dawehner asked for: rather than adding a new interface and trait, I'm instead changing things so that we just reuse
CacheableDependencyInterface
. This means that the cacheable HTTP exceptions become immutable rather than mutable, which seems far safer. On top of that, it means fewer interfaces, and more consistency with the rest of core!IOW:
extract a
CacheableDependencyTrait
fromRefinableCacheableDependencyTrait
remove
CacheableHttpExceptionInterface
andCacheableHttpException
(the base class)instead, let cacheable HTTP exceptions implement the already-existing-and-widely-implemented
CacheableDependencyInterface
, and to avoid excessive repetition in the implementations,use CacheableDependencyTrait
remove all changes to
CacheableResponseInterface
andCacheableResponseTrait
Comment #223
borisson_Nitpicks ahead:
80 cols by putting the class on the next line?
I understand that we need a difference here, but it looks like the only thing that is different is:
$expected_dynamic_page_cache_header_value
in the second calls is always false the second time.Do you think we can make this is there a way to make this more explicit?
I think that would make the reason for this if clearer.
80 cols
Can we put the comment above the start of the code? At first glance it looked like part of this code was commented out.
Same here.
80 cols
80 cols
80 cols.
Comment #224
Wim Leers#223: thanks so much for the review! ❤️
Comment #225
borisson_Awesome, just one question, this is tagged as performance and scalability but it doesn't have any numbers on the performance improvements made. Should we get those to prove that this actually makes things better?
Comment #226
Wim LeersSee the issue summary created by the original reporter: (s)he was reporting that 4xx responses for REST resources were not being cached. This patch makes those responses cacheable. Test coverage proves this, and provides has the correct cacheability metadata. Which improves scalability. It also means a repeated request for the same URL will result in a faster 4xx response, much like #2827797: ResourceResponse(Subscriber) + Dynamic Page Cache: making authenticated ResourceResponses significantly faster. That issue made authenticated REST responses 15% faster (on a simple use case — 15% is pretty much the minimal expected performance gain).
Comment #227
borisson_The reason why I asked is because I wanted to see if this would also mean a 15% increase or if it was lower/higher. I guess it doesn't really matter because we're sure that this is a performance improvement anyway.
I think this issue also means that we should reroll #2472335: Support an independent max-age for 4xx responses and make rest responses also use that configuration.
I read trough the entire patch again and couldn't find anything that i'd change, but leaving this for someone more knowledgable to rtbc.
Comment #228
Wim LeersI think you're qualified enough to RTBC this 😉
(Also: reviewed #2472335: Support an independent max-age for 4xx responses . There's nothing that patch needs to change to support this AFAICT. But once this issue lands, we can let #2472335: Support an independent max-age for 4xx responses add explicit REST test coverage in
EntityResourceTestBase
, and then we'll have absolute certainty!)Comment #229
Wim LeersThis is a pretty big patch, one that understandably will take quite some time to get reviewed & approved. So I'm looking at how we could break this patch up. (That doesn't mean this patch can't reach RTBC in its current state: it'd just become a fair bit smaller if this other patch lands first!)
I could extract these changes into a separate issue, but nothing would use this new trait.
I could extract these (and all other similar ones) to a separate issue, but nothing would use these new exceptions.
(The exceptions would use the trait though.)
However, the two bullets above, together with these changes … actually could end up being a sensible patch to extract from this issue.
This change actually needs to land independently anyway probably.
So then this can serve as the first use case, along with landing the necessary infrastructure (the various
Cacheable*Exception
classes). After that issue/patch lands, we can continue here, and the patch will be significantly smaller!If you want to help this issue land, then also see you in #2920001: Add cacheable HTTP exceptions: Symfony HTTP exceptions + Drupal cacheability metadata, where we will make it much easier to land this patch!
Comment #230
Wim Leers#2920001: Add cacheable HTTP exceptions: Symfony HTTP exceptions + Drupal cacheability metadata now has a reviewable (and hopefully RTBC'able) green patch. It's 31.8 KB. It'd cut the size of the patch in this issue down to about 40 KB, which obviously would make it much more focused and reviewable.
So I'm just going to postpone this issue on #2920001: Add cacheable HTTP exceptions: Symfony HTTP exceptions + Drupal cacheability metadata.
Comment #231
Wim Leers#2874938: AdminRouteSubscriber must only mark HTML routes as administrative has been committed, which means we can (and must) now remove much of the complexity in this patch:
All of this can be removed!
(That was not a blocker, this issue is still blocked on #2920001: Add cacheable HTTP exceptions: Symfony HTTP exceptions + Drupal cacheability metadata, but it does help make this patch much more understandable! 😀)
From 189 lines changed in
EntityResourceTestBase
to only 140! (And(Basic|Cookie)ResourceTestTrait
also each lost ~10 lines of changes.)Comment #232
Wim LeersAaaaand #2920001: Add cacheable HTTP exceptions: Symfony HTTP exceptions + Drupal cacheability metadata is in!
Straight rebase.
@borisson_, the honor to RTBC is yours :D
Comment #233
borisson_Read trough the patch again - just to make sure the rebase worked and I couldn't find anything to change. Looks solid!
Comment #235
Wim LeersAutomatic rebase courtesy of
git
.Comment #236
catchThis should load the config object and use Config::getCacheTags(). If there's a reason to not do that, should add a comment why. Everything else looks good.
Comment #237
Wim Leers👌 great nit! On it.
Comment #238
Wim LeersComment #239
borisson_Sorry Wim, one more nit.
Why is it config one time, and configuration the other? The rest of core seems to be using config factory more often, let's use that.
Comment #240
Wim LeersBoth were copy/pasted from
\Drupal\Core\EventSubscriber\FinalExceptionSubscriber
, but happy to fix!Comment #241
borisson_Awesome, thanks! RTBC++
Comment #244
Wim LeersRequestHandlerTest
failed, because I forgot to update it to inject the config factory service. Wim--Comment #246
Wim Leers#244 suddenly fails because #2800873: Add XML GET REST test coverage, work around XML encoder quirks landed. That added
ConfigurableLanguageXmlBasicAuthTest
andContentLanguageSettingsXmlBasicAuthTest
(among many other tests), and those two tests are now failing. Same problem as #217. Trivial fix :)Comment #247
webchickOk, Wim stepped me through this patch for about an hour. While it's big in size, the actual changes are fairly small, and mostly confined to updating tests.
I had the same question as @borisson_ about whether or not we had benchmarks for this, but Wim explained (as he did in #226) that this is merely applying the same pattern as #2827797: ResourceResponse(Subscriber) + Dynamic Page Cache: making authenticated ResourceResponses significantly faster so there's no sense in re-hashing this here.
Also nice that fixing this problem uncovered a couple of other subtle bugs with places where we were missing cacheability metadata. Rock! :D
I see @catch's concerns have been addressed, sooooo...
Committed and pushed to 8.5.x. Thanks!
Comment #249
webchickPushed, though I had to remove a missing use statement and undo some permission changes before it let me. :)
Comment #250
Wim LeersYay!
#249: sorry about that :(
I followed through on all the things:
Comment #251
Wim LeersAlso: was finally able to delete these branches:
😅
Comment #252
Wim LeersNice, this already helped prevent a potential security vulnerability from landing in #2889855-75: Unpublished media entity can not be accessed by owner and update any media/delete any media access possibly cached by user!
Comment #253
Wim Leers#2765959: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage took advantage of the APIs introduced here to bring that to core's REST module. #2929428: [>=8.5] Convert "throw new *HttpException" into "throw new Cacheable*HttpException" where possible aims to do the same for the contrib JSON API module.
Comment #255
Wim LeersThis so vastly improves the performance for certain REST API client use cases that I think it's worth a highlight.
Comment #256
davidwbarratt CreditAttribution: davidwbarratt commented+1 to that!