The issue affects any menu link that links to an internal page by its path alias. Pages that are linked to in this manner this will return a 404 when visiting the path alias after database updates are run (via update.php or drush, doesn't matter). The fix is to clear the menu router cache (part of cache_data bin).

This is quite easy to reproduce:

  • Create a page and give it an alias like "test-page"
  • Don't add that page to the menu via the node form
  • Add menu link to main menu (via menu management form) and specify the link as "/test-page" (don't use the autocomplete to look up page by title). This creates a "MenuLinkContent" entity with a URI like "internal:/test-page"
  • Run drush updb (or visit update.php, but you need to have a pending database update to run. Drush doesn't require that).
  • Observe that visiting /test-page now returns a 404.

The bug was introduced Drupal 8.7 via #3006086: update.php should not process path aliases. That issue disabled core's PathProcessorAlias path processor during database updates to prevent other issues. Disabling this path processor creates the problem as seen in this pseudo stack trace:

  • Database updates are finished running (see DbUpdateController::batchFinished)
  • All caches on site are cleared via drupal_flush_all_caches()
  • This calls \Drupal::service('router.builder')->rebuild()
  • Which fires off event RoutingEvents::FINISHED when it's finished
  • Which causes MenuRouterRebuildSubscriber to rebuild all menu links
  • When MenuLinkContentDeriver gets its turn to rebuild menu links that it provides, it loads a URL object for the URI that each menu link goes to (see MenuLinkContent::getPluginDefinition)
  • Because the PathProcessorAlias processor has been disabled, there's nothing to convert the path alias to a proper route, so it gets no route assigned to it (a 404)
  • The bad path to route conversion is cached because when a URL object tries to resolve an internal URL, it creates a fake request object with that path and calls RouteProvider::getRouteCollectionForRequest. This creates a cache entry for that alias in cache_data bin.

My brain is fried after debugging this one.

A short term solution could be to invalidate the "route_match" cache tag in a subscriber to RoutingEvents::FINISHED that executes AFTER the menu router rebuild subscriber.

Comments

bkosborne created an issue. See original summary.

esteinborn’s picture

I have begun experiencing this issue as well. Additionally, if there are no DB updates running update.php will still break pages and return 404s

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

bkosborne’s picture

Here's the code for a route subscriber that we're using to work around this:


namespace Drupal\ps_core\Routing;

use Drupal\Core\Cache\Cache;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Class RouterRebuildCacheFixSubscriber.
 *
 * Clears the route match cache after menu links are rebuilt.
 * This is to workaround a tricky core issue documented here:
 * https://www.drupal.org/project/drupal/issues/3075675.
 */
class RouterRebuildCacheFixSubscriber implements EventSubscriberInterface {

  /**
   * {@inheritdoc}
   */
  public function onRouterFinished() {
    Cache::invalidateTags(['route_match']);
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events = [];
    // Set priority just after MenuRouterRebuildSubscriber, which has the
    // code that could screw up the route data.
    $events[RoutingEvents::FINISHED][] = ['onRouterFinished', 99];
    return $events;
  }

}
stephen ollman’s picture

Where should this code be placed?

bkosborne’s picture

You would need to create a simple custom module for this code.

Webbeh’s picture

+1; our university may be encountering this on some of our standalone D8 websites.

chris burge’s picture

I've run into this on on a project, as well. @bkosborne - thanks for posting workaround code!

bkosborne’s picture

Issue tags: +Regression
stephen ollman’s picture

Created a module and enabled but still getting the same issue.

Run update.php -> 404 pages appear
Clear cache -> no 404 pages

bkosborne’s picture

Review this guide and make sure the event subscriber is working correctly. If it is, then you may have some other problem.

Webbeh’s picture

I would like to recommend, as a mediation (and if it is helpful), for the sample code to be spun into a sandbox project to help alleviate this issue in the interim.

chris burge’s picture

+1 re #12 - except, I'd make it a full module. Sometimes an issue like this lingers for years before a fix is committed to core.

Webbeh’s picture

Created a sandbox module for this: https://www.drupal.org/sandbox/webbeh/3090205

I would love to have a more-knowledgeable coder on this as a co-maintainer before deploying to a full module.

Webbeh’s picture

I've promoted the project to a full release: https://www.drupal.org/project/menu_router_rebuild_cache_fix

I would like to have an additional member from this issue to assist with the release on this - any takers?

amateescu’s picture

Status: Active » Closed (duplicate)

I was able to reproduce this with Drupal 8.7.x, but not with Drupal 8.8.x. It was most likely fixed by #3055443: Switch to a null backend for all caches for running the database updates, so I'm going to close this one as a duplicate.