Problem/Motivation
DomainSourceToken::getTokens() ends each replacement with $bubbleable_metadata->merge($generated_url); on line 70. BubbleableMetadata::merge() builds $result = parent::merge($other) and returns a new object; it does not modify the object it is called on. The return value is discarded here, so the merge has no effect at all.
The consequence is that the cache contexts, cache tags and max-age carried by $url->setAbsolute()->toString(TRUE) never bubble out of the token replacement. A rendered page containing a domain source token can therefore be cached without the cacheability the absolute cross-domain URL depends on, and served again under conditions where that URL should have been regenerated.
Steps to reproduce
Render any text containing a domain source token on a page and inspect the resulting render array cacheability, or read line 70: the generated URL's metadata is computed and then thrown away.
The defect is also visible on CI. On the next major lane the code triggers two failures from one line: PHPStan reports Call to method Drupal\Core\Render\BubbleableMetadata::merge() on a separate line discards return value with identifier method.resultDiscarded, and PHPUnit 12 raises the matching warning, which run-tests.sh turns into a job failure even though every test passes. The identifier is marked non-ignorable, so a baseline entry cannot silence it: the code has to change.
Proposed resolution
Use the in-place call, which is what core's own token hooks do. SystemTokensHooks uses this shape three times for the url, url-brief and login-url tokens, the last of which reads:
$result = Url::fromRoute('user.page', [], $url_options)->toString(TRUE); $bubbleable_metadata->addCacheableDependency($result); $replacements[$original] = $result->getGeneratedUrl();
domain_source already has the surrounding two lines and only needs the middle call corrected to addCacheableDependency().
Note that satisfying the static analysis by using the return value, as in $bubbleable_metadata = $bubbleable_metadata->merge($generated_url);, would leave the bug in place: the caller holds the original object, so reassigning the local variable does not propagate. addCacheableDependency() is the correct fix because it modifies the object the caller passed in.
Remaining tasks
Fix on 4.x and backport to 3.x and 3.0.x. Line 70 is identical on all three branches.
Test coverage asserting that the generated URL's cache contexts and tags reach the bubbleable metadata handed to the token replacement.
User interface changes
None.
API changes
None. No signature changes and no method is removed.
Data model changes
None, and no update hook is needed. The change is not a backwards compatibility break: BubbleableMetadata::addCacheableDependency() overrides the trait implementation to call addAttachments() as well when the argument implements AttachmentsInterface, and GeneratedUrl extends BubbleableMetadata, so cache contexts, cache tags, max-age and attachments all transfer, which is exactly what the discarded merge() would have produced. The one visible effect is the fix itself: pages that render a domain source token gain the cacheability they should always have carried, so caching becomes correctly less aggressive in some cases.
AI-Generated: Yes (Claude Code was used to help draft this issue summary. I reviewed it before posting; the finding came out of a CI run on an unrelated merge request and there is no merge request on this issue yet.)
Issue fork domain-3618104
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #5
mably commentedComment #6
mably commentedComment #10
mably commented