Problem
DomainSourceRouteMatcher::getRouteProvider() registers the active domain as the route provider's cache key part only on the first call in a process, because the provider is held in a static:
if (!isset(static::$routeProvider)) { static::$routeProvider = \Drupal::service('domain_source.route_provider'); if (static::$routeProvider instanceof CacheableRouteProviderInterface) { $domain_id = \Drupal::service('domain.negotiator')->getActiveId(); static::$routeProvider->addExtraCacheKeyPart('domain', $domain_id); } }
That is fine for a web request, which serves one domain. It is wrong for any process that changes the active domain part way through, such as cron generating a sitemap per domain with domain_simple_sitemap. The cache key stays as the first domain, while alias resolution follows the domain currently active. Every route lookup after the switch is stored under the first domain's key with another domain's resolved path.
A visitor on that domain later hits the poisoned entry. The route provider serves the cached path directly and never re-resolves the alias, so the request lands on a node belonging to a different domain. Depending on domain access this produces a 403 with no reason, or a redirect to the other domain via domain_source. A cache flush fixes it until the next cron run repopulates the entry, which makes it look intermittent.
Observed on a production site
Multi-domain site with domain_path and domain_simple_sitemap. After a cache flush, cron ran sitemap generation. Route entries for several shared aliases were written under the key of one domain (call it domain A), while a different domain was active at the time of each write:
| Cache ID path | Written | Correct for domain A | Active domain at write |
|---|---|---|---|
| /about-us | domain B's node | domain A's node | domain B |
| /our-people | domain B's node | domain A's node | domain B |
| /why-invest-us | domain B's node | domain A's node | domain B |
| /our-funds | domain C's node | domain A's node | domain C |
Stack at each write:
DomainSourceRouteProvider->getRouteCollectionForRequest DomainSourceRouteMatcher::routeMatch DomainSourcePathProcessor->processOutbound PathProcessorManager->processOutbound UrlGenerator->generateFromRoute Url->toString simple_sitemap UrlGeneratorBase->constructPathData domain_simple_sitemap DomainEntityUrlGenerator->generate simple_sitemap Generator->generate simple_sitemap_cron
Steps to reproduce
- Two or more domains sharing an alias, for example
/about-us, each mapped to its own node via domain_path. - domain_simple_sitemap enabled with a sitemap per domain.
drush cr, thendrush simple-sitemap:generatewith no--uri.- Inspect
cache.dataforroute:[domain]=<first domain>:...:/about-us. It holds a node belonging to a later domain.
The attached script wraps cache.data, runs sitemap generation, and reports every route entry written under a domain key whose path contradicts domain_path for that domain, together with the stack that wrote it.
Proposed resolution
Resolve the active domain where the route collection cache ID is built, which is where core adds its own language and query parameter parts, and for the same reason. Being computed at that point it cannot go stale, and no caller has to remember to register it. The static holding the provider goes with it, since it outlives the container it came from.
Entries already cached under the wrong domain are discarded by the cache flush that running database updates performs anyway, so no update hook is needed.
Merge requests: !461 for 4.x, !464 for 3.x, !465 for 3.0.x and !466 for 2.0.x. The route matcher is not needed on 4.x at all now that core 11.4 gives the path processor the route, which is #3623227.
AI-Generated: Yes (Claude Code was used to write the fix, its tests and this section of the summary. I reviewed them; the tests were confirmed to fail against the unfixed code on every branch, by the test-only job in each merge request pipeline.)
Related
The static was introduced in #3548531. #3280884 and #3154402 describe the same symptom, a domain's route cache holding another domain's node, and were closed as cannot reproduce. This is likely their cause.
The same static guard is present on 3.0.x, 3.1.0-alpha6 and 4.x, so this affects every current branch.
| Comment | File | Size | Author |
|---|---|---|---|
| #2 | domain-source-route-matcher-stale-domain-key.patch | 1.17 KB | sushyl |
Issue fork domain-3623187
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:
- 3623187-domainsourceroutematcher-caches-the-2.0.x
changes, plain diff MR !466
- 3623187-domainsourceroutematcher-caches-the-3.0.x
changes, plain diff MR !465
- 3623187-domainsourceroutematcher-caches-the-3.x
changes, plain diff MR !464
- 3623187-domainsourceroutematcher-caches-the
changes, plain diff MR !461
Comments
Comment #2
sushylComment #3
sushylComment #9
mably commentedComment #12
mably commentedComment #13
mably commentedHi @sushyl, can you give a try to MR 466 and tell me how it goes? Thanks.
Comment #15
sushylHi @mably,
Tested MR !466 on our site against the same reproduction that found the fault on production:
a full sitemap regeneration under a cache-write trace
Zero route entries written under the wrong domain, and every entry for the affected paths holds the correct node for its domain.
Confirmed the moved resolution point fixes it regardless of which caller reaches the provider. Thanks for the quick turnaround, and for the tests.
RTBC
Comment #17
mably commentedThanks @sushyl for the review! Let's merge this.