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

  1. Two or more domains sharing an alias, for example /about-us, each mapped to its own node via domain_path.
  2. domain_simple_sitemap enabled with a sitemap per domain.
  3. drush cr, then drush simple-sitemap:generate with no --uri.
  4. Inspect cache.data for route:[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.

Issue fork domain-3623187

Command icon 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

sushyl created an issue. See original summary.

sushyl’s picture

sushyl’s picture

Component: Code » - Domain Source
Priority: Normal » Major

mably made their first commit to this issue’s fork.

mably’s picture

Issue summary: View changes

  • mably committed 80e654b1 on 4.x
    fix: #3623187 DomainSourceRouteMatcher caches the domain route key once...

  • mably committed 514077c6 on 3.x
    fix: #3623187 DomainSourceRouteMatcher caches the domain route key once...
mably’s picture

Status: Active » Needs review
mably’s picture

Hi @sushyl, can you give a try to MR 466 and tell me how it goes? Thanks.

  • mably committed 9255ce3c on 3.0.x
    fix: #3623187 DomainSourceRouteMatcher caches the domain route key once...
sushyl’s picture

Status: Needs review » Reviewed & tested by the community

Hi @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

  • mably committed 98e5102c on 2.0.x
    fix: #3623187 DomainSourceRouteMatcher caches the domain route key once...
mably’s picture

Status: Reviewed & tested by the community » Fixed

Thanks @sushyl for the review! Let's merge this.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.