Problem/Motivation
\Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage provides local tasks for relevant entity types.
When viewing an entity of one of those types (let's just say node for simplicity), if overrides aren't enabled for the display for that node, as the links for the local tasks are built, the access for the layout_builder.overrides.node.view link has max-age 0. This max-age is applied to the local task block render array.
The reason this is happening is that as the local tasks are built, access to each link is checked via \Drupal\Core\Access\AccessManager::checkNamedRoute(). In that method, the route parameters for the layout_builder.overrides.node.view are upcasted, but since there is no overrides section storage for the entity, \Drupal\layout_builder\Routing\LayoutTempstoreParamConverter returns NULL for the section storage parameter, causing
\Drupal\Core\ParamConverter\ParamConverterManager::convert() to throw. ParamNotConvertedException
The exception is handled by the access manager by denying access and setting the access object cache max-age 0.
Since the local tasks block will vary by permissions and by each page anyway, making the block uncacheable is probably not wrong. However, it would affect page/response cacheability in #2352009: Bubbling of elements' max-age to the page's headers and the page cache.
Steps to reproduce
- Install Standard profile
- Install Layout Builder. Do not enable on any content type displays
- Create an Article
- Set container parameter
http.response.debug_cacheability_headerstotrue - View page for the created Article node and confirm
X-Drupal-Cache-Max-Ageheader is "0 (Uncacheable)" - Enable Layout Builder and overrides for Basic Page default display
- Create new Basic Page
- Visit new Basic Page and confirm
X-Drupal-Cache-Max-Ageheader is "-1 (Permanent)" - View page for the created Article node again and confirm
X-Drupal-Cache-Max-Ageheader is still "0 (Uncacheable)"
Proposed resolution
Per #22
- Don't gate ::extractEntityFromRoute() on the entity being fieldable, or having the override layout storage field. It would seem more appropriate to add a separate method for these checks if necessary (though I don't believe it to be necessary).
- Only permit access to the override layout section storage if the default section storage specifies that overrides are enabled. This will appropriately prevent access to the override layout editing route since LayoutBuilderAccessCheck already defers to section storage-specific access checks.
Remaining tasks
Review
User interface changes
NA
API changes
NA
Data model changes
NA
Release notes snippet
TBD
| Comment | File | Size | Author |
|---|
Issue fork drupal-3190542
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
godotislateComment #3
alexgreyhead commentedChipping-in/following this because this seems to be the reason why every single page on our Drupal 9 sites is uncacheable by our reverse proxy, platform.sh or Drupal.
If true, this is causing a huge additional load on our servers because every content page on our sites is uncacheable.
This would appear to be because https://www.drupal.org/project/cache_control_override is doing its job, but in doing so is silently killing page caching.
Disabling cache_control_override has fixed this problem for me.
/Al
Comment #6
tim.plunkettThanks to some discussion with @neclimdul in Slack, I think I can move this forward a bit.
Comment #7
neclimdulThanks to Tim I have the steps to reproduce.
1) Enable layout builder
2) Add overiddes to a content type(maybe optional)
3) View a node on a second type that doesn't use layout builder.
Since there are no errors and its all hidden in caches, setting the breakpoint at the exception in ParamConverterManager::convert is generally the easiest way I've found to observe the problem though a test would probably want to be testing the max-age from local task rendering or something..
Attached might actually be a fix. It basically uses the passed
$pathin the trait method to decide if the upcasting parameter is needed. By setting the value to false, the converter will return false in the upcasting applies method during the route building and then it won't run during normal conversion.I'm not sure about the AccessCheck change. Maybe it shouldn't be attached either but the change made things work and the signature change should be fine since its @internal.
Comment #8
neclimdulbah, cross posting. sorry Tim.
Comment #9
tim.plunkettComment #10
tim.plunkettWhoops, stupid mistakes.
Comment #13
tim.plunkettThe change to when tempstore is run is breaking a lot of stuff, so that's not right. Unassigning for now, but the tests are there.
Pushed up what was in the patch to a new MR.
Comment #14
neclimdulSo sent the morning mulling over this a bit instead of just hammering on code. I think there are basically two problems here which we've kinda danced around in the slack equate to two possible solutions.
Problems:
1) Layout builder is using parameter conversion as a hidden route enhancement to inject the section storage parameters into access control and controllers. This doesn't exactly align with the intention of parameter conversion which means it kinda runs into a problem with the interface.
2) Parameter conversion treats a null value as a failed conversion which means its impossible to have optional parameters like this. A first it seems kinda crazy to exclude null but it actually has a reason baked into the assumption of the API which we see if we dig a bit deeper into the history. Initially conversion explicitly threw a NotFound 404 exception[1]. Later, a more specific exception was bolted on[2] to allow conversion in other context like access control checks during task generation. In hindsight this API feels clunky and limiting but the intention was to specifically support converting the value for the slug
/node/{node}/viewinto a node object and if that fails it makes sense that the page doesn't exist and the controller etc shouldn't have to do a lot. It makes the design for a converter easier as well because it can just return the result of the object loading and offload all the handling of what "failing" to load the object means.Solutions:
Now personally I kinda lean toward 1.1. There isn't a lot of logic so it should be fairly easy to recreate in places. The problem I think is going to be that other enhancement methods are going to be less "integrated" into route magic and its going to be less simple that it should be.
1.2 mostly is ugly because the class would need to implement SectionStorageInterface which is gigantic and I'm not sure how we'd make "null" versions of 90% of the methods which makes it less generic and more a special hack only for this problem.
2 seems like maybe the "right" option but there is no guarantee we even _can_ change it because the assumptions kinda work for the intended purpose. If we do change it I doubt we'd see something usable for several versions which leaves a ton of nodes pages with uncacheable tasks. Personally I don't like this because tasks can be pretty heavy right now with #2950869: Entity queries querying the latest revision very slow with lots of revisions being triggered on route checks when content moderation is installed making node pages with the two modules installed very slow on large sites.
[1] #1798214: Upcast request arguments/attributes to full objects
[2]#2185831: Split up ParamConverterManager and stop throwing NotFoundHttpException
Comment #15
tim.plunkettNote also #3032433: Allow section storages to be loaded via routing without loading from the tempstore which wants to change a lot of the same code.
Comment #18
clayfreemanI believe that MR !1075 should be suitable to address the concerns in this issue in a way that doesn't create a dependency on the work being done in #3032433: Allow section storages to be loaded via routing without loading from the tempstore. Setting to Needs Review (pending test results).
Comment #19
tim.plunkettI retested the patch from #10 before rereading more and realizing that didn't work at the time, let alone now.
I ran out of time to really dig in on this, I'm not sure why the two MRs are so far apart. Either way, they should likely take advantage of the new
layout_builder_section_storageflag added in #3032433: Allow section storages to be loaded via routing without loading from the tempstoreComment #21
clayfreemanComment #22
clayfreemanRebased on 9.4.x/HEAD.
I spent several hours reviewing your feedback and the changes in #3032433: Allow section storages to be loaded via routing without loading from the tempstore; I'm sticking with my original plan of attack with MR !1075 for the following reasons:
NULLregardless of those changes, which is the crux of this issue. The parameter converter works for bothlayout_builder_section_storageandlayout_builder_tempstore(which is already present on the routes in question). The route enhancer only serves to replace the section storage with the copy stored in the temp store, and it won't do so if the parameter converter producesNULL.::loadEmpty()in the parameter converter and move the context derivation to a route enhancer, it makes us more vulnerable to future bugs that are potentially worse (e.g., passing an "empty" section storage to the controller which results in a white page -- or all pending changes being discarded), and could drastically complicate the API (since we would be missing some of the parameters that are passed to::deriveContextsFromRoute()). Not to mention, I don't believe there's any sort of weighting mechanism for route enhancers (though I could be wrong since documentation on this subsystem is sparse).My alternative approach is very simple:
::extractEntityFromRoute()on the entity being fieldable, or having the override layout storage field. It would seem more appropriate to add a separate method for these checks if necessary (though I don't believe it to be necessary).Fix theSee #24 instead.layout_builder_is_activecache context, as it currently doesn't work and will stand in the way of achieving the goals in the issue summary.LayoutBuilderAccessCheckalready defers to section storage-specific access checks.Moving to Needs Review for @tim.plunkett's feedback.
Comment #23
godotislate@clayfreeman Note that there is an issue to fix the
layout_builder_is_activecache context: #3190541: LayoutBuilderIsActiveCacheContext not providing correct context value.Comment #24
clayfreemanPer #23, I removed the changes to the cache context. I agree that it's out of scope, and I think its removal shouldn't negatively affect the prospects of resolving this issue since all tests should still pass.
Not moving to Postponed on #3190541: LayoutBuilderIsActiveCacheContext not providing correct context value. unless maintainers see it as a hard blocker.
Comment #25
clayfreemanLooks like the cache context interferes with something else, so marking this as Postponed on #3190541: LayoutBuilderIsActiveCacheContext not providing correct context value. anyway.
:(
Comment #26
clayfreemanChild issue was committed, so unpostponing.
Comment #29
bburgHello, Just testing the patch for MR 1075, and I immediately get the following error:
I also tested the patch in 791, and it still seems to miss the page cache.
Comment #30
smustgrave commentedThis still needs subsystem review
But the MR should be updated for 10.1 now and left a small change on the current MR.
Thanks.
Comment #32
bburgI feel this should be a higher priority. The bug completely breaks page cache on every non-layout builder page. I had to rip layout builder out of a site because of this.
Comment #33
larowlanPer #32
Comment #34
acbramley commentedThis is slightly wrong, it clobbers it on every page with Layout builder enabled without overrides enabled.
This took me an extremely long time to track down. Kudos to @larowlan for knowing the issue off hand.
Comment #36
godotislateCreated a new MR against 11.x based on @clayfreeman's MR.
Tweaked the access logic to forbidden if the default section storage is not overridable, because returning access neutral otherwise did not seem to resolve the cache issue.(I was mistaken about the logic change and removed it.)Comment #37
godotislateComment #38
godotislateComment #39
smustgrave commentedRebased to run the test-only feature which failed as expected
Issue summary still seems incomplete though. For sections that don't apply putting NA usually helps. Put TBD in sections
Comment #40
godotislateIS updated.
Comment #41
smustgrave commentedTHanks! Will still need submaintainer review but think it's ready for that now.
Comment #42
wim leersThen this isn't RTBC? 🤔😅
Subsystem maintainer @tim.plunkett worked on this until #10. In #19 he wrote that that approach cannot work.
In #22, @clayfreeman proposed a wholly new approach, resulting in the currently RTBC MR.
Therefore this should still get the sign-off from subsystem maintainer Tim Plunkett before this is RTBC.
Comment #43
tim.plunkettThanks @clayfreeman and @godotislate for keeping this one going.
Comment #44
catchCommitted/pushed to 11.x and cherry-picked to 10.2.x, thanks!