diff --git a/src/Illuminate/CachedRouting/Router.php b/src/Illuminate/CachedRouting/Router.php index f7125203..b4e41d5f 100644 --- a/src/Illuminate/CachedRouting/Router.php +++ b/src/Illuminate/CachedRouting/Router.php @@ -85,8 +85,18 @@ public function cache($filename, Closure $callback, $cacheMinutes = 1440) $cacher = $this->getRouteCacher(); $cacheKey = $this->getCacheKey($filename); - // Check if the current route group is cached. - if (($cache = $cacher->get($cacheKey)) !== null) { + // Route caching is best-effort: an unreadable/corrupt entry or an + // unwritable cache directory (e.g. permissions, or a torn write from + // concurrent boots after a deploy) must never break application boot. + // On any cache I/O failure we fall back to defining the routes directly. + $cache = null; + try { + $cache = $cacher->get($cacheKey); + } catch (\Throwable $e) { + // Treat an unreadable cache as a miss and rebuild below. + } + + if ($cache !== null) { $this->routes->restoreRouteCache($cache); } else { // Back up current RouteCollection contents. @@ -95,9 +105,13 @@ public function cache($filename, Closure $callback, $cacheMinutes = 1440) // Call closure to define routes that should be cached. call_user_func($callback, $this); - // Put routes in cache. - $cache = $this->routes->getCacheableRoutes(); - $cacher->put($cacheKey, $cache, $cacheMinutes); + // Persist the routes, ignoring failures so a broken cache store + // never propagates out of boot (routes stay defined in memory). + try { + $cacher->put($cacheKey, $this->routes->getCacheableRoutes(), $cacheMinutes); + } catch (\Throwable $e) { + // Best-effort cache; a write failure is non-fatal. + } // And restore the routes that shouldn't be cached. $this->routes->restoreRouteCollection(); diff --git a/tests/CachedRouting/RoutingIntegrationTest.php b/tests/CachedRouting/RoutingIntegrationTest.php index fa06ebb3..631e84f6 100755 --- a/tests/CachedRouting/RoutingIntegrationTest.php +++ b/tests/CachedRouting/RoutingIntegrationTest.php @@ -390,4 +390,48 @@ public function testCanClearCache(): void $router->clearCache(__FILE__); static::assertFalse($this->app->cache->has($key), 'Routes must no longer be cached'); } + + public function testRebuildsWhenCachedFileIsCorrupt(): void + { + $router = $this->getRouter(); + $router->cache(__FILE__, function () use ($router) { + $router->get('/', 'HomeController@actionIndex'); + }); + + // Simulate a torn/partial write: keep a far-future expiry prefix so the + // entry is not treated as expired, but leave an unserializable body. + foreach (glob(self::$cachePath . '/*/*/*') as $file) { + file_put_contents($file, '9999999999corrupt-payload'); + } + + $rebuilt = false; + $router = $this->getRouter(); + $router->cache(__FILE__, function () use ($router, &$rebuilt) { + $rebuilt = true; + $router->get('/', 'HomeController@actionIndex'); + }); + + static::assertTrue($rebuilt, 'Corrupt cache must trigger a rebuild, not a failure'); + static::assertEquals(1, $router->getRoutes()->count(), 'Routes must be rebuilt from the callback'); + } + + public function testBootStillWorksWhenCacheIsUnwritable(): void + { + // Point the cache at a path that cannot be created (a file where a + // directory is expected), so the underlying write fails — mirroring an + // unwritable cache directory on a production node. + $files = new Filesystem; + $files->makeDirectory(self::$cachePath, 0777, true, true); + $blocker = self::$cachePath . '/blocker'; + file_put_contents($blocker, 'x'); + $this->app['config']['cache.path'] = $blocker . '/nested'; + + $router = $this->getRouter(); + $key = $router->cache(__FILE__, function () use ($router) { + $router->get('/', 'HomeController@actionIndex'); + }); + + static::assertNotNull($key, 'cache() must return normally despite the write failure'); + static::assertEquals(1, $router->getRoutes()->count(), 'Routes must still be defined when caching fails'); + } }