Problem/Motivation
Domain negotiation currently runs as a KernelEvents::REQUEST event subscriber at priority 256. HTTP middlewares that read Drupal config — such as CleanTalk (priority 210) for example — execute before the active domain is set. This means domain_config overrides don't apply, and those middlewares always get the default config instead of domain-specific config.
Example of a Drupal middleware priority stack:
- 300 — ReverseProxy
- 250 — CORS / Ban
- 210 — CleanTalk
- 200 — PageCache
- 100 — KernelPreHandle (loads modules, pushes request to stack)
- 50 — Session
Event subscribers (including DomainSubscriber) only fire after all middlewares have run.
Proposed resolution
Extract domain negotiation into a lightweight HTTP middleware at priority 220 (after ReverseProxy which corrects HTTP_HOST from proxy headers, before CleanTalk at 210).
The middleware temporarily pushes the request to RequestStack, calls DomainNegotiator::getActiveDomain(), then pops it. No loadAll() is needed — in Drupal 11, DomainAliasHooks uses #[Hook('domain_request_alter')] so OOP hooks dispatch via the container without loading .module files.
DomainSubscriber stays unchanged — getActiveDomain() checks isNegotiated() and returns the cached domain, so the event subscriber's negotiation call becomes a no-op while redirect/access logic runs as before.
Remaining tasks
- Create
DomainNegotiationMiddlewareindomain/src/StackMiddleware/ - Register it in
domain.services.ymlashttp_middlewareat priority 220 - Test with domain_alias, domain_config, and modules that read config at middleware time
Issue fork domain-3575069
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:
Issue fork domain_extras-3575069
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 #2
mably commentedMove domain negotiation into HTTP middleware
Problem
Domain negotiation currently runs as a
KernelEvents::REQUESTevent subscriber at priority 256. Any HTTP middleware that reads Drupal config — such as CleanTalk at priority 210 — does so before the active domain is set, because event subscribers only fire after the full middleware stack has executed. This meansDomainConfigFactoryOverridereturns default config instead of domain-specific overrides for those middlewares.Approach
Add a new
DomainNegotiationMiddleware(http_middleware, priority 220) that negotiates the active domain early in the middleware stack. Priority 220 places it:HTTP_HOSTis already corrected from proxy headersdomain_configoverrides are active when CleanTalk reads configThe middleware:
RequestStacksoDomainNegotiatorreads the proxy-corrected hostname (not raw$_SERVER)DomainNegotiator::getActiveDomain()to negotiateHttpKernelpushes it again laterDomainSubscriberis unchanged —getActiveDomain()checksisNegotiated()and returns the cached domain, so the event subscriber's negotiation call becomes a no-op while its redirect/access logic runs as before.Execution flow after the change
HTTP_HOSTfrom proxy headersdomain_configoverrides work ✓loadAll(), push request (negotiation already cached)KernelEvents::REQUESTgetActiveDomain()returns cached domain (no-op); redirect/access logic runs as beforeModule loading and Drupal version handling
Domain negotiation triggers entity type discovery via
EntityTypeManager::getStorage('domain')and hook dispatch viaModuleHandler::alter('domain_request', ...). The middleware handles this differently depending on Drupal version:#[LegacyHook]procedural functions are the registered hook implementations. The middleware callsModuleHandler::loadAll()unconditionally before negotiation so that.modulefiles are available (e.g.domain_alias_domain_request_alter()). This is idempotent — the laterKernelPreHandlecall is a no-op..modulefiles. The middleware attempts negotiation withoutloadAll(). On a warm entity type cache (99.9% of requests), this means zero overhead. On a cold cache (e.g. duringupdate.php), entity type discovery needs proceduralhook_entity_type_buildimplementations — the middleware catches the failure, callsloadAll(), and retries.Changes
domain/src/StackMiddleware/DomainNegotiationMiddleware.phpdomain/domain.services.yml— added the middleware service definitionDomainSubscriber,DomainNegotiatorComment #4
mably commentedComment #5
mably commentedComment #6
mably commentedComment #8
mably commentedNew
domain_early_negotiationsubmoduleMoved the early domain negotiation middleware from the domain module into a standalone submodule in domain_extras. Enabling the module = enabling the feature — no separate config toggle needed.
Files
domain_early_negotiation.info.ymldomain:domaindomain_early_negotiation.services.ymldomain_early_negotiation.module#[LegacyHook]delegate for Drupal 10.6 compatibilitydomain_early_negotiation.routing.yml/admin/config/domain/early-negotiationdomain_early_negotiation.links.menu.ymlconfig/install/domain_early_negotiation.settings.ymlconfig/schema/domain_early_negotiation.schema.ymlsrc/StackMiddleware/DomainNegotiationMiddleware.php$enabledflag)src/DomainEarlyNegotiationServiceProvider.phpsrc/EventSubscriber/ConfigSubscriber.phpsrc/Form/DomainEarlyNegotiationSettingsForm.phpsrc/Hook/DomainEarlyNegotiationHooks.phphook_domain_config_ui_disallowed_configurations_alter()to prevent per-domain overrides of this module's settingstests/src/Kernel/DomainEarlyNegotiationTest.phpdocs/domain_early_negotiation/index.mdKey design decisions
enabledconfig flag — installing the module is the toggle. This eliminates the container parameter overhead when the feature is not wanted.DomainEarlyNegotiationServiceProvider, so there is zero runtime config read overhead.hook_domain_config_ui_disallowed_configurations_alter()— middleware priority must be the same across all domains.Comment #10
mably commented