Problem/Motivation
Drupal 11.4 introduced two independent core changes that each break purl in a distinct way. Both are fixed on this branch, which requires core_version_requirement: ^11.4 (see companion 2.x issue #3608542, which keeps purl working unmodified on core <11.4).
1. Subrequest invisible to Drupal's request_stack service
Starting with Drupal 11.4, core's http_kernel.basic service (Symfony\Component\HttpKernel\HttpKernel) is constructed with null for its RequestStack argument instead of Drupal's real @request_stack service (see the comment on that service definition in core.services.yml). Symfony's raw HttpKernel then keeps its own private, internal RequestStack for push()/pop(), completely separate from Drupal's request_stack service.
RequestSubscriber::onRequest() strips a purl prefix and processes the result as a Symfony subrequest by calling $event->getKernel()->handle($request, HttpKernelInterface::SUB_REQUEST). $event->getKernel() is always the raw Symfony HttpKernel - RequestEvent is constructed with $this inside HttpKernel::handleRaw(), never the outer StackedHttpKernel/DrupalKernel. Calling handle() on it directly therefore bypasses DrupalKernel::preHandle() and the KernelPreHandle middleware entirely, and on 11.4+ makes the subrequest invisible to Drupal's request_stack service for its entire lifecycle - so any code asking "what's the current request/route" during that subrequest gets the main request instead (which has no route yet, since the subrequest short-circuited routing on the main request).
Confirmed: Drupal 10.1, 10.3, 10.4, 11.0, and 11.3 all pass Drupal's real @request_stack into http_kernel.basic; only 11.4 changed that argument to null.
Symptoms: Error: Call to a member function getOptions() on null in PurlNodeContextRoutes::purlCheckNodeContext(); fatal errors in any code reading \Drupal::routeMatch() during a purl-processed pageview (core's HtmxContentViewSubscriber, custom breadcrumb builders, etc.); in combination with group_purl's keep_context handling, infinite redirect loops on already-correctly-prefixed group URLs (see group_purl issue #3608553).
2. PurlRouteProvider constructor incompatible with core's new RouteProvider signature
Separately, Drupal 11.4 (#3503843, route cache refactor) inserted a new $fast_cache parameter into Drupal\Core\Routing\RouteProvider::__construct(), before the existing $table parameter:
__construct(Connection $connection, StateInterface $state, CurrentPathStack $current_path, CacheBackendInterface $cache_backend, InboundPathProcessorInterface $path_processor, CacheTagsInvalidatorInterface $cache_tag_invalidator, $fast_cache = NULL, $table = 'router', ?LanguageManagerInterface $language_manager = NULL)
Core itself ships a graceful BC shim for this (if $fast_cache isn't a CacheBackendInterface, it re-shifts the remaining arguments with a deprecation notice) - but that shim lives in core's own constructor, and PurlRouteProvider's subclass constructor, which has its own explicit, positionally-typed parameter list (ending in $table, $language_manager, $contextHelper, $matchedModifiers), never reaches it. On Drupal 11.4+, the DI container now passes the new 9-argument core list plus purl's own +2 = 11 positional arguments, which land on the wrong parameters of purl's old, unshifted constructor - concretely, the string 'router' (meant for the new $table slot) lands in the $language_manager parameter, throwing:
TypeError: PurlRouteProvider::__construct(): Argument #8 ($language_manager) must be of type ?LanguageManagerInterface, string given
This is fatal on every single request (route provider is constructed on essentially every bootstrap), and its failure during Drupal's exception-teardown path (re-entering the same not-yet-constructed service via maintenance_mode_subscriber -> url_generator -> purl.url_generator -> router.route_provider) additionally surfaces as a ServiceCircularReferenceException, which is a symptom of the same root TypeError, not a separate bug.
Steps to reproduce
- Install purl on a site running Drupal core 11.4 or later.
- Run any Drush command or visit any page (e.g.
drush cr, or any request that constructsrouter.route_provider). - Observe the
TypeErrorabove (or, if that happens to be caught, theServiceCircularReferenceException). - Separately: configure a path-prefix modifier/provider, visit a URL matching that prefix, and observe the subrequest-visibility symptoms described above.
Proposed resolution
For the subrequest issue, in src/Event/RequestSubscriber.php:
- Manually push the subrequest onto Drupal's
request_stackservice before callinghandle($request, SUB_REQUEST), and pop it in afinallyblock - unconditionally, since this branch requires core>=11.4. - After the subrequest completes, copy a narrow allowlist of route-matching attributes (
_route,_route_object,_route_params,_raw_variables,_controller,_format) back onto the main request - not a bulk copy, to avoid leaking access-check results or cache-context data.
In src/Event/PurlNodeContextRoutes.php, purlCheckNodeContext(): return early when !$event->isMainRequest(), and treat a null getRouteObject() as a no-op rather than a fatal error.
For the RouteProvider constructor issue, in src/Routing/PurlRouteProvider.php: replace the explicit, positionally-typed constructor with a variadic one that pops purl's own trailing arguments ($matchedModifiers, then $contextHelper) off the argument list and forwards everything else, unchanged and in order, to parent::__construct(...$args). This makes the constructor agnostic to exactly how many positional arguments core's router.route_provider service passes - correct whether core provides 8 arguments (pre-11.4) or 9 (11.4+, with $fast_cache) - with no version-detection code needed at all.
Set purl.info.yml core_version_requirement: >=11.4.
Remaining tasks
- Apply the
RequestSubscriber/PurlNodeContextRouteschanges (already committed on this branch) - Apply the
PurlRouteProvidervariadic-constructor fix (new) tests/src/Unit/PurlNodeContextRoutesTest.php- verifies theisMainRequest()guard and null-route guard. Confirmed to fail with the exact original error against pre-fix code; passes against the fix.tests/src/Kernel/PurlSubrequestRequestStackTest.php- confirms the subrequest resolves to itself, not the main request. Confirmed to fail against pre-fix code; passes against the fix.- Add a test covering the
PurlRouteProviderconstructor against both the pre-11.4 and 11.4+ argument counts core's DI container would pass, to guard against future core signature changes recurring here - group_purl companion fix (separate issue):
NodeGroupViewControllerself-redirect prevention forkeep_contextnodes - #3608553 - Bump
core_version_requirementto>=11.4 - Release as 3.0.0
User interface changes
None.
API changes
Behavioral only — no public method signatures change. Sites with custom code reading $request->attributes during a purl-prefixed pageview should be aware the main request now also carries a narrow set of route-matching attributes copied from the subrequest.
Data model changes
None.
Issue fork purl-3608029
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
freelockComment #4
freelockNeeds new 3.0.x branch, and review for plugins other than group_purl subrequests.
Comment #5
freelockUpdating IS, which was missing info about a circular dependency issue that gets hit first...
Comment #6
freelockThe fix so far is still broken for Ajax requests -- particularly this breaks media library, on pages where Purl is active.
Comment #7
freelockAdditional commit now fixes media library on pages modified by Purl.