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

  1. Install Standard profile
  2. Install Layout Builder. Do not enable on any content type displays
  3. Create an Article
  4. Set container parameterhttp.response.debug_cacheability_headers to true
  5. View page for the created Article node and confirm X-Drupal-Cache-Max-Age header is "0 (Uncacheable)"
  6. Enable Layout Builder and overrides for Basic Page default display
  7. Create new Basic Page
  8. Visit new Basic Page and confirm X-Drupal-Cache-Max-Age header is "-1 (Permanent)"
  9. View page for the created Article node again and confirm X-Drupal-Cache-Max-Age header is still "0 (Uncacheable)"

Proposed resolution

Per #22

  1. 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).
  2. 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

Issue fork drupal-3190542

Command icon 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

godotislate created an issue. See original summary.

godotislate’s picture

Issue summary: View changes
alexgreyhead’s picture

Chipping-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

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

tim.plunkett’s picture

Assigned: Unassigned » tim.plunkett
Priority: Minor » Normal
Issue tags: +Blocks-Layouts, +Needs steps to reproduce, +Needs tests

Thanks to some discussion with @neclimdul in Slack, I think I can move this forward a bit.

neclimdul’s picture

Assigned: tim.plunkett » Unassigned
Priority: Normal » Minor
Issue tags: -Needs steps to reproduce
StatusFileSize
new1.83 KB

Thanks 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 $path in 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.

neclimdul’s picture

Assigned: Unassigned » tim.plunkett
Priority: Minor » Normal

bah, cross posting. sorry Tim.

tim.plunkett’s picture

Issue summary: View changes
Status: Active » Needs review
Issue tags: -Needs tests
StatusFileSize
new2.01 KB
new4.45 KB
new4.24 KB
tim.plunkett’s picture

Whoops, stupid mistakes.

Status: Needs review » Needs work

The last submitted patch, 10: 3190542-local_task-10-PASS.patch, failed testing. View results

tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned

The 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.

neclimdul’s picture

So 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}/view into 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:

  1. Refactor LayoutTempstoreParamConverter.
    1. Refactor _some_ of the logic out of the converter and only have the converter doing converting and maybe do some sort of other enhancement or other logic.
    2. Provide some sort of null/static/invalid section storage that could be returned from the conversion that is not null and can full fill the needs of things like the access control API even if its never injected into a controller.
  2. 2) Change the upcasting/parameter conversion API to allow null values.

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

tim.plunkett’s picture

clayfreeman made their first commit to this issue’s fork.

clayfreeman’s picture

Status: Needs work » Needs review

I 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).

tim.plunkett’s picture

I 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_storage flag added in #3032433: Allow section storages to be loaded via routing without loading from the tempstore

Status: Needs review » Needs work

The last submitted patch, 10: 3190542-local_task-10-PASS.patch, failed testing. View results

clayfreeman’s picture

Version: 9.3.x-dev » 9.4.x-dev
clayfreeman’s picture

Status: Needs work » Needs review
Issue tags: +Needs subsystem maintainer review

Rebased 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:

  1. Unless I'm missing something obvious, the work that was committed from #3032433: Allow section storages to be loaded via routing without loading from the tempstore doesn't seem to afford us anything that is material to resolving this issue. The parameter converter still produces NULL regardless of those changes, which is the crux of this issue. The parameter converter works for both layout_builder_section_storage and layout_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 produces NULL.
  2. It doesn't seem wise to add yet another parameter converter since we'll still end up with the exact same problem. (The access check and route enhancement doesn't happen until parameter conversion is finalized.)
  3. If we opt to stop at ::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:

  1. 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).
  2. Fix the layout_builder_is_active cache context, as it currently doesn't work and will stand in the way of achieving the goals in the issue summary. See #24 instead.
  3. 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.

Moving to Needs Review for @tim.plunkett's feedback.

godotislate’s picture

@clayfreeman Note that there is an issue to fix the layout_builder_is_active cache context: #3190541: LayoutBuilderIsActiveCacheContext not providing correct context value.

clayfreeman’s picture

Per #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.

clayfreeman’s picture

Status: Needs review » Postponed

Looks like the cache context interferes with something else, so marking this as Postponed on #3190541: LayoutBuilderIsActiveCacheContext not providing correct context value. anyway.

:(

clayfreeman’s picture

Status: Postponed » Needs review

Child issue was committed, so unpostponing.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

bburg’s picture

Hello, Just testing the patch for MR 1075, and I immediately get the following error:

"RuntimeException: Callable "Drupal\layout_builder\Access\LayoutBuilderAccessCheck::access" requires a value for the "$section_storage" argument. in Drupal\Component\Utility\ArgumentsResolver->handleUnresolvedArgument() (line 143 of core/lib/Drupal/Component/Utility/ArgumentsResolver.php)."

I also tested the patch in 791, and it still seems to miss the page cache.

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative

This still needs subsystem review

But the MR should be updated for 10.1 now and left a small change on the current MR.

Thanks.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

bburg’s picture

I 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.

larowlan’s picture

Priority: Normal » Major

Per #32

acbramley’s picture

The bug completely breaks page cache on every non-layout builder page

This 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.

godotislate’s picture

Issue summary: View changes

Created 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.)

godotislate’s picture

Status: Needs work » Needs review
godotislate’s picture

Issue summary: View changes
smustgrave’s picture

Issue summary: View changes
Status: Needs review » Needs work
Issue tags: +Needs issue summary update

Rebased to run the test-only feature which failed as expected

There were 2 errors:
1) Drupal\Tests\layout_builder\Functional\LayoutBuilderLocalTaskTest::testLocalTaskLayoutBuilderInstalledCacheability
Behat\Mink\Exception\ExpectationException: Current response header "X-Drupal-Cache-Max-Age" is "0 (Uncacheable)", but "-1 (Permanent)" expected.
/builds/issue/drupal-3190542/vendor/behat/mink/src/WebAssert.php:794
/builds/issue/drupal-3190542/vendor/behat/mink/src/WebAssert.php:161
/builds/issue/drupal-3190542/core/modules/layout_builder/tests/src/Functional/LayoutBuilderLocalTaskTest.php:62
/builds/issue/drupal-3190542/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
2) Drupal\Tests\layout_builder\Functional\LayoutBuilderLocalTaskTest::testLocalTaskMultipleContentTypesCacheability
Behat\Mink\Exception\ExpectationException: Current response header "X-Drupal-Cache-Max-Age" is "0 (Uncacheable)", but "-1 (Permanent)" expected.
/builds/issue/drupal-3190542/vendor/behat/mink/src/WebAssert.php:794
/builds/issue/drupal-3190542/vendor/behat/mink/src/WebAssert.php:161
/builds/issue/drupal-3190542/core/modules/layout_builder/tests/src/Functional/LayoutBuilderLocalTaskTest.php:103
/builds/issue/drupal-3190542/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
ERRORS!

Issue summary still seems incomplete though. For sections that don't apply putting NA usually helps. Put TBD in sections

godotislate’s picture

Issue summary: View changes
Status: Needs work » Needs review

IS updated.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs issue summary update

THanks! Will still need submaintainer review but think it's ready for that now.

wim leers’s picture

Assigned: Unassigned » tim.plunkett
Status: Reviewed & tested by the community » Needs review

Will still need submaintainer review

Then 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.

tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned
Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs subsystem maintainer review

Thanks @clayfreeman and @godotislate for keeping this one going.

catch’s picture

Version: 11.x-dev » 10.2.x-dev
Status: Reviewed & tested by the community » Fixed

Committed/pushed to 11.x and cherry-picked to 10.2.x, thanks!

  • catch committed 038e6515 on 10.2.x
    Issue #3190542 by godotislate, clayfreeman, tim.plunkett, neclimdul,...

  • catch committed 40a48863 on 11.x
    Issue #3190542 by godotislate, clayfreeman, tim.plunkett, neclimdul,...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.