Problem/Motivation
While working on #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method, I noticed it was unfortunately necessary to rebuild the router whenever you imported (i.e. deployed) RestResourceConfig config entities. This should happen automatically thanks to a ConfigEvents::SAVE event subscriber.
Worse, you actually even need to call drupal_flush_all_caches(): just calling $this->container->get('router.builder')->rebuild() is insufficient! (I have no idea why yet.)
When doing an import through the UI, this means you need to go to /admin/config/development/performance and click the Clear all caches
button. If a developer forgets this step, then their REST routes won't work.
Proposed resolution
- Routing system:
- Add
http_responsecache tag to all cacheable responses. - Let route rebuilder invalidate the
http_response_cache tag after every rebuild.
- Add
- REST module:
- Make
RestResourceConfig::post(Save|Delete)mark the router as needing a rebuild. - Implement a REST config save event subscriber (for the
rest.settingsconfig) that marks the router as needing a rebuild.
- Make
Together this means that:
- every router rebuild will now invalidate all responses. Cached 404 responses may become 200s, 200s may become 403s, 404s may become 403s, and so on. Hence all cached responses need to be rebuilt.
- whenever
RestResourceConfigconfig entities orrest.settingssimple config change, mark the router as needing a rebuild, which will trigger the above.
Remaining tasks
None.
User interface changes
None.
API changes
None.
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #76 | 2815845-76.patch | 30.47 KB | alexpott |
| #76 | 71-76-interdiff.txt | 560 bytes | alexpott |
| #71 | 2815845-71.patch | 30.68 KB | wim leers |
| #71 | interdiff-64-71.txt | 1.08 KB | wim leers |
| #68 | interdiff.txt | 1.02 KB | dawehner |
Comments
Comment #2
dawehnerIMHO the right thing would be to call to
\Drupal\Core\Routing\RouteBuilder::setRebuildNeededComment #3
wim leersWFM
Comment #4
swentel commentedOMG, you probably just saved me endless drupal_flush_all_caches() in custom tests!
Comment #5
wim leers#4: You're doing a lot of tests with REST resources?
Comment #6
dawehnerThere we go.
Comment #8
wim leersOh, interesting! I thought this would need a
\Drupal\Core\Config\ConfigEvents::SAVEsubscriber.Is this better? Why? Should we document that?
(Asking for the patch, but also because I'd genuinely like to know.)
\Drupal\dblog\Tests\Rest\DbLogResourceTest.This is why:
I'm afraid we really need this router to be rebuilt too, because otherwise the test cannot access the router to generate URLs to access.
BrowserTestBasetest that tests as a regular user: making config changes causes route changes to be available on the next request.Comment #9
dawehnerI just copied it from views. I think the rule might be like the following: if your particular plugin inside the entity needs this change, go with a subscriber. If its needed for every of those entity usages, go with
::postSaveLet's see whether conditional rebuilds might help.
Comment #10
wim leersSounds good.
+1
Comment #12
wim leersBetter, but not there yet.
Fails with
Call to a member function getResourcePlugin() on a non-object, only for POST and PATCH (so GET and DELETE work fine). This means that routing toRequestHandler::handle()works, but this results in no entity being loaded:Interesting.
Comment #13
wim leersRebased.
Comment #14
wim leersSince #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method, there's another place where we need to remove the forced rebuilding:
ResourceTestBase.Comment #15
wim leersThis is what also needed to be removed per #14.
Comment #17
dawehnerI tried this out, certainly didn't helped.
Comment #19
dawehnerThis could fix a good amount of the failures, well, when invalidating the router. Note: We need to invalidate page_cache && dynamic_page_cache on a router rebuild.
Comment #21
wim leersREST resource responses have
config:rest.resource.*cache tags, corresponding to theRestResourceConfigentities. But that doesn't help with cached 4xx responses, and that's what's going on here.Wouldn't the more elegant solution therefore be to ensure that all 4xx responses for REST have the
RestResourceConfiglist cache tag?Comment #22
dawehnerConceptually for me the route_match cache tag should be on every response. Technically route rebuilding could have arbitrary sideeffects.
Comment #23
wim leersI just remembered that emptying Page Cache & Dynamic Page Cache explicitly is not an acceptable solution, because the same cannot be done for reverse proxies.
So, whatever we do, it must only use cache tag-based invalidation.
That's a good point. So should we add a cache tag for that to all responses then?
Comment #24
berdirI think we discussed before that we should have a cache tag that allows us to invaidate all internal and external "full-page" caches. if route_match works for that fine with me.
Comment #25
wim leers#24: exactly.
We've used the
renderedcache tag for that purpose, but it only works for HTML responses. This new cache tag would work for all responses.Perhaps we want an even more generic cache tag? For example,
drupal? There are other reasons to invalidate all responses, such as deploying new modules, in which caseroute_matchis a poor name.Comment #26
dawehnerWhat about using
http_response, as this adds a little bit more semanticness?Here is an experimental patch which will see how many failures we could get.
Comment #27
dawehnerComment #29
dawehnerThis should/could fix a lot of the failures ...
Comment #31
wim leers#26: sounds good!
#29: patch is looking good. Just two remarks for now:
Shouldn't this also be done for responses to subrequests?
This change looks like a debugging leftover?
Comment #32
dawehnerThank you for the review wim!
It totally is indeed!
Great point!
Let's see whether I caught every instance ...
Comment #34
dawehnerThis should fix a good amount of them :)
Comment #36
dawehnerLet's see :)
Comment #38
dawehnerComment #40
wim leersSo close!
Comment #41
dawehnerI debugged for a while. I think this should fix all the remaining issues ..
Comment #42
dawehnerIgnore the last comment. I totally messed up creating the patch files.
Comment #43
wim leersThis is looking excellent. I have only nitpicks basically.
But why 16? Why not 7, or 77? Let's document the rationale for this number.
<3
This can be deleted,
\Drupal\Tests\rest\Functional\ResourceTestBase::setUp()is already taking care of that :)It does this:
This is a debug leftover.
Comment #44
dawehnerThank you for your review wim!
If there would be just one :)
So some debugging latter ... we need to rebuild the router also when we delete resource configs, otherwise old entries stick in there. Sadly this causes some other failures later.
Comment #45
wim leersThis "16" is still a mystery to me.
Oops :)
Why do we need this now? :( Because the router used by the test itself is otherwise outdated?
This can now also be deleted :)
Comment #47
dawehnerThe tests won't pass anyway ... :)
For me as well. Well, I had no reason beside it being > 0 + some distance in case someone wants to add some more cache tags.
Yes. Before deleting the routes the node hal_json route is in there.
Comment #49
dawehner... Alright, so some failures seems to be related with line 317 still being cached:
Alright, why is this still cached?
in
\Drupal\Core\Cache\DatabaseCacheTagsChecksum::invalidateTags. When you invalidate cache tags multiple times, which happens when you rebuild the router multiple times in the test, the$this->invalidatedTagsmap has our tag stored. This means that invalidating the caches a second time on a second results in no invalidation.One solution would be to reset the internal states, which happens in
core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php:211as well.Comment #50
wim leersI think that's the only solution then.
I'd create a new issue and add a TODO to both places that use this hack that points to that issue. That new issue would say something like . What do you think? I'd be happy to create that issue for you.
Comment #51
dawehnerThat would make sense, well, to be honest our test is executing code both on a production system as well as on the test instance. This is the main issue we deal with. Too bad that we will probably never be able to deploy rest configuration via a POST request, as we kinda have a dependency problem.
Comment #52
wim leersActually, reading the explanation + interdiff in #49 again and also reading my concerns/criticism about that in #50 again, makes me disagree with myself.
I agree with @dawehner's assessment in #51:
There's no way around that.
But I do think we can make that a bit more clear:
These are the critical changes: these ensure that routes are rebuilt when necessary :)
Hurray!
Let's rephrase this so that it's clear that we need to rebuild this solely for the purpose of the route building we do _inside_ the test (i.e. the tester, not the testee).
This one is oddly inconsistent with the changes in
ResourceTestBase. This indicates we should also be able to updateResourceTestBaseto userebuildIfNeeded(). Doing so locally shows it works.I addressed all of my own feedback. IMO it can be RTBC'd now. But it still needs a CR (working on that now).
Comment #53
wim leersCR created: https://www.drupal.org/node/2847072
Comment #54
dawehnerThe latest interdiff totally works for me. I love new methods with a proper name.
Comment #55
wim leersCool :)
RTBC then. I've been reviewing this patch, and dawehner just reviewed the sole interdiff I posted. (#13 was a straight rebase, #15 was just removing the
@todothat pointed to this issue. @dawehner did all the work.)Comment #57
tedbowThis patch looks good to me. +1 the RTBC, was a little take a back by the number file but most are simply adding 'http_response' to $expected_tags for test response.
Should make test run faster right? yay!
Comment #58
tedbowActually I am still seeing @todo's pointing to this issue. 4 in EntityResourceTestBase.php
Like this.
The are not after saving REST resource config objects so maybe we still need the drupal_flush_all_caches() call?
But we should remove the todo's if so
The postSave changes is tested by the fact that after \Drupal\Tests\rest\Functional\ResourceTestBase::provisionResource all the routes are avaiable for the tests.
But for postDelet() what confirms the route is being rebuilt after deleting a REST resouce? Is this important?
At the end of each of the test*() methods in we could \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase delete the resource and confirm we get a 404.
Comment #59
wim leersThanks for the extra diligence, @tedbow! I can't believe I missed those additional
@todos back in #15.Modifying
rest.settings'bc_entity_resource_permissionscauses routes to change. The changes inRestResourceConfig::(post|pre)savetrigger a route rebuild when REST Resource Config entities are modified. But whenrest.settings.bc_entity_permissionsis modified, we also need to trigger a route rebuild!All the remaining
@todos are aboutrest.settings.bc_entity_permissions.First, let's prove that what I wrote here is actually true. This should fail: this should result in 200 responses instead of 403 responses because routes are not being rebuilt and hence the BC entity permissions are not required, which is why it's resulting in 200 responses instead of 403.
Comment #60
wim leersThis adds a
ConfigEvents::SAVEsubscriber that callssetRebuildNeeded()so that the router is rebuilt also whenrest.settings.bc_entity_permissionsis modified.This should be green again.
Comment #61
wim leersThe first line here is not modifying a REST Resource Config entity. It's modifying
rest.settings, which is just Simple Configuration.Hence the name of the method called on the last line is not a great match.
IOW: these comments and the method name should be modified.
Addressed that.
Comment #63
wim leers@tedbow pointed out in chat that this is not yet addressed:
Of course he is right.
\Drupal\Tests\rest\Functional\ResourceTestBase::setUp()does this:This deletes the default REST resource config entities: the one for node. So we’re already testing it, just not explicitly.
Let's do this right.
Comment #64
wim leersDone.
Comment #67
dawehnerI'm looking into those failures
Comment #68
dawehnerHere is a fix. As explained in the codecomment, we actually expect a 406.
Comment #70
webflo commentedThe 403 is comes from AuthenticationSubscriber::onKernelRequestFilterProvider. The AuthenticationSubscriber runs before the route access check. @dawehner had an idea how to handle this edge for BasicAuth but i could not follow.
Comment #71
wim leersActually, that 406 vs 403 behavior was encountered before, and there already is a helper method to deal with that. This behavior is being fixed in #2805279: Routing system + authentication system + format-specific routes (e.g. those in rest.module) = frustrating, unhelpful 403 responses instead of 406 responses.
Comment #72
wim leersComment #73
dawehnerNice fix!
Comment #74
wim leersThis is now also blocking #2844046: REST Resource config entities do not respect the status (enabled/disabled).
Comment #75
wim leersThis is also causing problems in the REST UI contrib module. It blocks the following fixes:
Comment #76
alexpottNeeded a re-roll - fixed and unused use too.
Comment #77
wim leersComment #78
alexpottDiscussed with @Berdir, @swentel and @Wim Leers in IRC.
I raised a concern about invalidating all http_responses on router rebuild. @Berdir and @Wim Leers pointed out that with routes appearing and disappearing you need to invalidate these tags to ensure that users get the expected 200 or 404. If this does not occur they'll get stale content. We discussed with the RouterBuilder could be made smarter and only clear affected URLs but this would require wildcard cache clearing which is just not possible and more granular cache tags would be very expensive. Out of the discussion I asked for two followups:
node_form_system_themes_admin_form_submit()that should be moved to a config listener and only done if the value is changed.Unfortunately the are no easy wins here. Thanks @Wim Leers for updating the issue summary to outline the consequences of this change.
Comment #79
wim leersCreated this follow-up: #2851414: Many RouteBuilder::setRebuildNeeded() calls only occur when using the UI, not when using the configuration system. (It's not so much a follow-up to this issue as a realization that this issue triggered.)
Comment #80
alexpottCommitted and pushed 172e9e3 to 8.4.x and 2dcf1bc to 8.3.x. Thanks!
Comment #82
wim leersI got #2844046: REST Resource config entities do not respect the status (enabled/disabled) and #2851127: REST UI module should not be rebuilding routes going again :)
And #2851126: The UI says "disable", but it's really "delete" now has one less blocker!
Comment #83
xjmComment #85
quietone commentedpublish the change record