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
- A controller uses the common pattern
$resultPage->getConfig()->getTitle()->set(...). Page\Config::getTitle() triggers a full early layout build.
- 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.
Result\Page::render() → publicBuild() → the swapped-in builder (isBuilt = false) re-runs loadLayoutUpdates() / generateLayoutXml() / generateElements() on the same shared layout instance.
- 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.
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 sharedReader\Contextto the generator pool, which consumed theScheduledStructurein 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 secondgenerateElements()re-interprets the layout XML on top of that dirty state, andLayout\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
$resultPage->getConfig()->getTitle()->set(...).Page\Config::getTitle()triggers a full early layout build.Magento\Framework\App\ViewInterface(e.g.Magento\Customer\Helper\Session\CurrentCustomer, injected by a block or view model).App\View::__constructeagerly runs$pageFactory->create(true); the throwawayResult\Pageconstructs aPage\Builder, whose constructor unconditionally calls$this->pageConfig->setBuilder($this)on the sharedPage\Config— replacing the already-built builder.Result\Page::render()→publicBuild()→ the swapped-in builder (isBuilt = false) re-runsloadLayoutUpdates()/generateLayoutXml()/generateElements()on the same shared layout instance.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\Viewand the builder swap inPage\Builder::__constructare 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.additionalchildren contributed by the theme'sdefault_hyvahandle (head.js,head.hyva-scripts,speculationrules) vanish from the page, breaking allhyva.*JavaScript on affected routes (e.g. third-party customer-account controllers that set the title early and then render blocks usingCurrentCustomer). 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'sScheduledStructureand the layout'sData\StructurewhengenerateElements()runs on an already-built layout), or prevent the rogue rebuild at its sources: createApp\View's page lazily ingetPage()instead of the constructor, and/or don't letPage\Builder::__constructreplace an already-built builder on the sharedPage\Config.Workaround
DI-detach
App\View's eager page from the sharedPage\Configfor injection points that are constructed mid-build: a virtualResult\PagewithisIsolated=trueplus a non-sharedPage\Config(via a virtualTemplate\Context), wired intoApp\View'spageFactoryargument 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.