Skip to content

Runtime assignment in a non-pair partial leaks slot => null, blanking a later partial's {{ slot }} #14833

@jurnskie

Description

@jurnskie

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:

  1. the page-level {{? $foo = 'bar' ?}} assignment,
  2. the filter partial's own {{? $values = 'v' ?}} assignment,
  3. 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.

Metadata

Metadata

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions