You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
mark() refuses unrelated #[Cacheable] siblings later in the same request #234
A mark() refuses every #[Cacheable] write later in the same request, including unrelated siblings.
Behaviour (1.x 062bbde, found in the 1.18.0 release review)
A #[DonutCache] page embeds a hole that issues a CSRF token (UncacheableMarkInterface::mark()) and an unrelated #[Cacheable] hole. When the unrelated hole is evaluated after the mark, its own write is refused (put_skipped{reason: unstorable}) and its onGet() re-runs on every such request. Evaluated before the mark, or fetched in a request without a mark, it is stored normally; there is no lasting poisoning (verified: the next unmarked request stores it, the one after hits).
Cause
QueryRepository::doPut() asks UnstorableGateInterface::reason(), which uses UncacheableMarkInterface::isMarked() (any owner, anywhere in the request). This is deliberately conservative and documented: QueryRepository cannot see the resource call tree, and a #[Cacheable] parent that embeds a marked child must be refused. Only a #[DonutCache] write's own template uses the scoped isMarkedForCurrentScope().
Direction
Refuse only the ancestor chain of the marking resource (track the executing resource as a scope stack around each #[Cacheable] execution; a write is refused when a mark was made in its own subtree). Must keep every #222 guarantee, incl. render-time marks and nested holes (see #231's review rounds).
Workaround until then: place cacheable siblings before the marking hole in the template, so they are evaluated (and stored) before the mark.
Closing as obsolete: #242 removed the unreleased mark() machinery (UncacheableMarkInterface, UnstorableGate, ...) entirely, so there is no request-wide mark left to refuse unrelated #[Cacheable] siblings. In 1.18.0, QR trusts the cache attribute as the declaration that a response is shared, and per-user values belong in a non-cacheable #[DonutCache] hole (see the README).
Activity
Broaden the sibling-collateral-refusal limitation note to CacheableRe…
Preserve backward compatibility for HeaderSetter and the repository c…
Scope a direct put()'s own-write refusal to its frame's object identity
CHANGELOG: note that a direct put() stays conservative after bearsund…
Split UncacheableMarkInterface into app-facing and package-internal h…
Scope HeaderSetter's own mark-vs-header check to match reasonForOwnWr…
Add permanent tests verifying a direct put() inside own onGet() (bear…
Append $mark after $writeObserver in QueryRepository's constructor
Fix put_skipped's code field contract; rename fixture; qualify CHANGELOG
koriym commentedon Oct 28, 2026
Closing as obsolete: #242 removed the unreleased
mark()machinery (UncacheableMarkInterface,UnstorableGate, ...) entirely, so there is no request-wide mark left to refuse unrelated#[Cacheable]siblings. In 1.18.0, QR trusts the cache attribute as the declaration that a response is shared, and per-user values belong in a non-cacheable#[DonutCache]hole (see the README).