Problem/Motivation

DefaultExceptionHtmlSubscriber::makeSubrequest() gives router.no_access_checks a cloned request context with its method set to GET, so it can route to the error page. It never restores the original context afterwards. This means that POST is matched to a stale GET.

This affects anything that handles more than one request with one container, such as kernel tests and persistent application servers like FrankenPHP worker mode.

PathValidator::getAttributes() already restores the router context after it changes it.

Steps to reproduce

This behavior appears when one container handles multiple requests (for example, FrankenPHP in worker mode, or a script that calls http_kernel repeatedly).

  1. Install Drupal with the Standard profile to get the basic_html editor and POST-only route ckeditor5.upload_image.
  2. Save this as repro.php and run with drush php:script repro.php:
    <?php
    use Symfony\Component\HttpFoundation\Request;
    
    $kernel = \Drupal::service('http_kernel');
    foreach ([1, 2] as $i) {
      $request = Request::create('/ckeditor5/upload-image/basic_html', 'POST');
      $response = $kernel->handle($request);
      echo "Request $i: " . $response->getStatusCode() . PHP_EOL;
    }
    $context_is_shared = \Drupal::service('router.no_access_checks')->getContext()
      === \Drupal::service('router.request_context');
    echo 'Router uses shared context: ' . var_export($context_is_shared, TRUE) . PHP_EOL;
    

Expected: Both requests return 403, because access is denied. The router still uses the shared router.request_context service.

Actual:

Request 1: 403
Request 2: 405
Router uses shared context: false

The 403 in request 1 is rendered by DefaultExceptionHtmlSubscriber::makeSubrequest(). That method gives the router a cloned context with its method set to GET and never restores the original. RouterListener keeps updating the shared router.request_context for each new request, but the router now holds the detached clone. Router::doMatchCollection() checks $this->context->getMethod(), so request 2 is matched as a GET, fails the methods: [POST] requirement, and returns 405 Method Not Allowed. The router also keeps checking the host against the detached context from request 1.

Proposed resolution

Save the router's context before the subrequest and restore it in a finally block. This way, it is restored even if route matching or the subrequest throws.

Issue fork drupal-3626006

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

ptmkenny created an issue. See original summary.

ptmkenny’s picture

Status: Active » Needs review
smustgrave’s picture

Status: Needs review » Needs work

Thank you for reporting. Could we complete the IS please. "See the test" isn't really a valid summary of steps and will ultimately delay the ticket.

ptmkenny’s picture

Issue summary: View changes
ptmkenny’s picture

Issue summary: View changes
ptmkenny’s picture

Status: Needs work » Needs review

@smustgrave Sorry about that; I'll make sure to write out the steps for future core issues.

smustgrave’s picture

Much appreciated! :)

amitgoyal’s picture

Status: Needs review » Reviewed & tested by the community

Verified locally against the reproduction script. Both requests now return 403, and the router context stays shared (previously request 2 returned 405).

Pipeline is green apart from the allowed-failure PHP 8.6 unit job, unrelated to this change. Test coverage passes.

Moving to RTBC.