Problem

Since #3575227 moved DomainSourcePathProcessor from priority 90 to 310, it now runs before the language path processor (priority 100). This means $options['language'] is not yet populated when DomainSourcePathProcessor::processOutbound() executes.

When the entity is loaded by ID from route parameters (e.g. during route normalization via Url::fromRoute('<current>')), the processor gets the default translation. Without $options['language'], it cannot switch to the correct translation. If the default translation has a different domain_source than the current translation, this incorrectly triggers a cross-domain rewrite, causing a 301 redirect to the wrong domain.

Steps to reproduce

  1. Enable domain, domain_source, domain_path, redirect, and language modules
  2. Create two domains (Domain 1 and Domain 2)
  3. Create a node with:
    • Default (EN) translation: domain_source = Domain 1
    • French translation: domain_source = Domain 2
  4. Visit the French translation URL on Domain 2
  5. The redirect module's route normalizer generates the canonical URL, which incorrectly resolves to Domain 1 (from the default translation)
  6. Result: 301 redirect to Domain 1 with the wrong alias → 404

Root cause

In DomainSourcePathProcessor::processOutbound(), the translation resolution at lines 182–193 only runs when $options['language'] is set. At priority 310, the language path processor (priority 100) has not yet populated this option. The entity stays as the default translation, and getSourceDomain() returns the wrong domain.

Fix

When $options['language'] is not available and the site is multilingual, fall back to LanguageManagerInterface::getCurrentLanguage(TYPE_CONTENT) to determine the current content language. This was the effective behavior before the priority change (when the language processor ran first at 100, then DomainSourcePathProcessor at 90).

A kernel test is included that reproduces the bug (fails without the fix, passes with it).

Issue fork domain-3575489

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

mably created an issue. See original summary.

mably’s picture

Title: DomainSourcePathProcessor translated content regression » DomainSourcePathProcessor uses wrong translation when $options['language'] is not set (priority 310)
Issue summary: View changes

mably’s picture

Status: Active » Needs review
mably’s picture

Component: Code » - Domain Source

  • mably committed e8cfa2d8 on 3.x
    fix: #3575489 DomainSourcePathProcessor uses wrong translation when $...
mably’s picture

Status: Needs review » Fixed

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.

idebr’s picture

An alternative implementation is to add a second Outbound processor to domain_source at priority: 90 that sets the $options['domain'] to the correct translated one based on the $options['language'] provided by the language negotiation plugin. It is not as clean as providing all logic in a single processor, but it does allow the default language negotiation plugins to apply their logic

mably’s picture

I thought the domain had to be determined before calling the domain_path path processor...

And the domain comes potentially from a translation of the content as the domain source field is translatable. So we need the language before that.

Unless I miss something.

mably’s picture

Actually we will have the same problem with DomainPathAliasProcessor (from domain_path) that also expects the $options['language'] to be defined...

public function processOutbound($path, &$options = [], ?Request $request = NULL, ?BubbleableMetadata $bubbleable_metadata = NULL) {
    // Skip external URLs or paths without a domain target.
    if (!empty($options['external']) || !isset($options['domain'])) {
      return $path;
    }

    // The domain option must be a DomainInterface entity.
    $domain = $options['domain'];
    if (!$domain instanceof DomainInterface) {
      return $path;
    }

    $langcode = isset($options['language']) ? $options['language']->getId() : NULL;
    ...

Looks like we went a bit too fast on that one.

It still works because DomainPathAliasManager does this in getAliasByPathAndDomain:

    $method = $this->getConfig()->get('language_method') ?: LanguageInterface::TYPE_CONTENT;
    $langcode = $langcode ?: $this->languageManager->getCurrentLanguage($method)->getId();
mably’s picture

AliasManager defaults to TYPE_URL:

  public function getAliasByPath($path, $langcode = NULL) {
    if (!str_starts_with($path, '/')) {
      throw new \InvalidArgumentException(sprintf('Source path %s has to start with a slash.', $path));
    }
    // If no language is explicitly specified we default to the current URL
    // language. If we used a language different from the one conveyed by the
    // requested URL, we might end up being unable to check if there is a path
    // alias matching the URL path.
    $langcode = $langcode ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId();
    ...

May be we should do the same in DomainPathAliasManager and DomainSourcePathProcessor.

What do you think @idebr?

idebr’s picture

Hmm, I would expect the target language to be determined before resolving the path alias.

Disclaimer: I don't manage any projects with both Domain and multi-lingual, so I have to do a deep dive into the current implementation in Core for some insight

mably’s picture

@idebr an idea of how to fix that problem once and for all?

Here is another related issue: #2973694: Outbound path processor does not have access to language and then redirects

Should we increase the priority of the PathProcessorLanguage for outbound instead?

mably’s picture

mably’s picture

Actually the only language negotiation outbound processor that sets the $options['language'] seems to be the LanguageNegotiationUrl one.

https://git.drupalcode.org/project/drupal/-/blob/11.3.3/core/modules/lan...

So using TYPE_URL probably gives the same result.

Status: Fixed » Closed (fixed)

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