Skip to content

Re-entrant layout builds silently corrupt page structure since ACP2E-4040 (isolated reader context) #330

Description

@rhoerr

Summary

Since 6194581 (ACP2E-4040, imported from upstream Magento 2.4.9), building the same shared layout twice within one request silently corrupts the generated structure: blocks scheduled into a container by an <update>-included handle lose their parent and disappear from the rendered page.

Before that change, Magento\Framework\View\Layout::generateElements() handed the shared Reader\Context to the generator pool, which consumed the ScheduledStructure in place — after a build it was empty, so a (rare but possible) second build re-interpreted into clean state. Since ACP2E-4040, generation runs on a clone (createIsolatedReaderContext()), so after a build the shared context retains all scheduled rows and materialized paths. A second generateElements() re-interprets the layout XML on top of that dirty state, and Layout\ScheduledStructure\Helper::_overrideElementWorkaround() treats the re-read declarations as overrides of the leftover rows — silently destroying the children scheduled under their paths.

How a double build happens with core code only

  1. A controller uses the common pattern $resultPage->getConfig()->getTitle()->set(...). Page\Config::getTitle() triggers a full early layout build.
  2. During that build, block generation lazily instantiates a class depending on the deprecated Magento\Framework\App\ViewInterface (e.g. Magento\Customer\Helper\Session\CurrentCustomer, injected by a block or view model). App\View::__construct eagerly runs $pageFactory->create(true); the throwaway Result\Page constructs a Page\Builder, whose constructor unconditionally calls $this->pageConfig->setBuilder($this) on the shared Page\Config — replacing the already-built builder.
  3. Result\Page::render()publicBuild() → the swapped-in builder (isBuilt = false) re-runs loadLayoutUpdates() / generateLayoutXml() / generateElements() on the same shared layout instance.
  4. The second pass corrupts the structure. Log fingerprint (developer mode):
    Broken reference: the 'google_gtag_analytics' tries to reorder itself towards 'head.hyva-scripts', but their parents are different: 'head.additional' and '' respectively.

The eager page creation in App\View and the builder swap in Page\Builder::__construct are long-standing quirks; they were harmless while a second build started from a consumed (empty) reader context. ACP2E-4040 made re-entrant builds destructive.

Observed impact

On a Hyva storefront (Mage-OS 3): the head.additional children contributed by the theme's default_hyva handle (head.js, head.hyva-scripts, speculationrules) vanish from the page, breaking all hyva.* JavaScript on affected routes (e.g. third-party customer-account controllers that set the title early and then render blocks using CurrentCustomer). Any layout where a handle included via <update handle="..."/> schedules blocks into a re-declared container is susceptible.

Suggested direction

Either make re-entrant builds safe again (reset the shared Reader\Context's ScheduledStructure and the layout's Data\Structure when generateElements() runs on an already-built layout), or prevent the rogue rebuild at its sources: create App\View's page lazily in getPage() instead of the constructor, and/or don't let Page\Builder::__construct replace an already-built builder on the shared Page\Config.

Workaround

DI-detach App\View's eager page from the shared Page\Config for injection points that are constructed mid-build: a virtual Result\Page with isIsolated=true plus a non-shared Page\Config (via a virtual Template\Context), wired into App\View's pageFactory argument for e.g. CurrentCustomer::$view.

Identified on a client site with Mage-OS 3.4 + Hyva Theme + Amasty B2B suite, on an account-area controller.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions