Bug description
A {{? ?}} runtime assignment placed inside a non-pair partial leaks that partial's slot => null into GlobalRuntimeState::$tracedRuntimeAssignments while assignment tracing is armed. When a later pair partial (one that actually receives slot content) runs any tag in its body before echoing {{ slot }}, NodeProcessor::setData() merges the traced slot => null back over the pair partial's scope and blanks its {{ slot }} output.
The pair partial's slot is intact when its view starts; it is nulled only after the in-body tag runs while the trace still carries the leaked slot. The partial's cascade is otherwise fine (e.g. {{ id }} / {{ title }} still resolve) — only the default {{ slot }} is clobbered. Named slots ({{ slot:foo }}) are unaffected because they use a different key.
It is silent: HTTP 200, no exception, nothing logged.
How to reproduce
Three templates:
page.antlers.html
{{? $foo = 'bar' ?}}
{{ partial:filter }}
{{ partial:card }}KEEPME{{ /partial:card }}
filter.antlers.html — non-pair, so its own slot is null
{{? $values = 'v' ?}}FILTER
card.antlers.html — pair; runs a tag, then echoes its slot
{{ trans key='hi' }}<slot>{{ slot }}</slot>
Render page.
- Expected:
...<slot>KEEPME</slot>
- Actual:
FILTERhi<slot></slot> — {{ slot }} is empty.
Removing any one of the three ingredients makes KEEPME render correctly:
- the page-level
{{? $foo = 'bar' ?}} assignment,
- the filter partial's own
{{? $values = 'v' ?}} assignment,
- the tag (
{{ trans }}) in the card body before {{ slot }}.
Real-world trigger that surfaced this: a mounted-collection overview page where a taxonomy-filter partial (non-pair, containing {{? $values = request()->input(...) ?}}) was rendered before the entry cards. Each card renders an image via {{ glide }} (a tag) and then {{ slot }}, so every card rendered with its image but a completely blank body.
Note: the trace state lives in process-global statics on GlobalRuntimeState that are only reset on ResponseCreated / RequestHandled (via Statamic\Listeners\ClearState), so within a single render the leak crosses partial boundaries. (When reproducing via repeated view()->render() calls in one process — e.g. a test — reset with GlobalRuntimeState::resetGlobalState() between renders.)
Logs
None — no exception is thrown and nothing is logged.
Environment
- Statamic: reproduced on v6.20.0 and v6.21.0
- PHP 8.4
- Laravel 12
Installation
Reproducible with plain flat-file content and no add-ons (the {{ trans }} repro above needs no database, assets, or packages). Happy to provide a minimal reproduction repository if useful.
Bug description
A
{{? ?}}runtime assignment placed inside a non-pair partial leaks that partial'sslot => nullintoGlobalRuntimeState::$tracedRuntimeAssignmentswhile assignment tracing is armed. When a later pair partial (one that actually receives slot content) runs any tag in its body before echoing{{ slot }},NodeProcessor::setData()merges the tracedslot => nullback over the pair partial's scope and blanks its{{ slot }}output.The pair partial's
slotis intact when its view starts; it is nulled only after the in-body tag runs while the trace still carries the leakedslot. The partial's cascade is otherwise fine (e.g.{{ id }}/{{ title }}still resolve) — only the default{{ slot }}is clobbered. Named slots ({{ slot:foo }}) are unaffected because they use a different key.It is silent: HTTP 200, no exception, nothing logged.
How to reproduce
Three templates:
page.antlers.htmlfilter.antlers.html— non-pair, so its ownslotisnullcard.antlers.html— pair; runs a tag, then echoes its slot{{ trans key='hi' }}<slot>{{ slot }}</slot>Render
page....<slot>KEEPME</slot>FILTERhi<slot></slot>—{{ slot }}is empty.Removing any one of the three ingredients makes
KEEPMErender correctly:{{? $foo = 'bar' ?}}assignment,{{? $values = 'v' ?}}assignment,{{ trans }}) in the card body before{{ slot }}.Real-world trigger that surfaced this: a mounted-collection overview page where a taxonomy-filter partial (non-pair, containing
{{? $values = request()->input(...) ?}}) was rendered before the entry cards. Each card renders an image via{{ glide }}(a tag) and then{{ slot }}, so every card rendered with its image but a completely blank body.Note: the trace state lives in process-global statics on
GlobalRuntimeStatethat are only reset onResponseCreated/RequestHandled(viaStatamic\Listeners\ClearState), so within a single render the leak crosses partial boundaries. (When reproducing via repeatedview()->render()calls in one process — e.g. a test — reset withGlobalRuntimeState::resetGlobalState()between renders.)Logs
None — no exception is thrown and nothing is logged.
Environment
Installation
Reproducible with plain flat-file content and no add-ons (the
{{ trans }}repro above needs no database, assets, or packages). Happy to provide a minimal reproduction repository if useful.