From d23130ad121a9b1201b440011160c79283e4511a Mon Sep 17 00:00:00 2001 From: Joshua Blum Date: Tue, 2 Jun 2026 18:11:16 +0200 Subject: [PATCH 1/2] Scope shared static cache errors to site --- src/StaticCaching/ServiceProvider.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/StaticCaching/ServiceProvider.php b/src/StaticCaching/ServiceProvider.php index 3cb2726cc17..960a14ffec8 100644 --- a/src/StaticCaching/ServiceProvider.php +++ b/src/StaticCaching/ServiceProvider.php @@ -8,6 +8,7 @@ use Illuminate\Support\ServiceProvider as LaravelServiceProvider; use Illuminate\Support\Str; use Statamic\Facades\Cascade; +use Statamic\Facades\Site; use Statamic\StaticCaching\NoCache\DatabaseSession; use Statamic\StaticCaching\NoCache\Session; @@ -97,7 +98,10 @@ public function boot() }); Request::macro('fakeStaticCacheStatus', function (int $status) { - $url = '/__shared-errors/'.$status; + // Namespace the shared error by the current site so that multisite + // installs serve a correctly localized error page per site, rather + // than whichever site happened to render the error first. + $url = '/__shared-errors/'.Site::current()->handle().'/'.$status; $this->pathInfo = $url; $this->requestUri = $url; app(Session::class)->setUrl($url); From b67ebbdd95d3c1b8a98b543f5179512df4c34f09 Mon Sep 17 00:00:00 2001 From: Joshua Blum Date: Tue, 2 Jun 2026 18:16:07 +0200 Subject: [PATCH 2/2] Add test --- .../SharedErrorsStaticCachingTest.php | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests/StaticCaching/SharedErrorsStaticCachingTest.php diff --git a/tests/StaticCaching/SharedErrorsStaticCachingTest.php b/tests/StaticCaching/SharedErrorsStaticCachingTest.php new file mode 100644 index 00000000000..71a5daaae27 --- /dev/null +++ b/tests/StaticCaching/SharedErrorsStaticCachingTest.php @@ -0,0 +1,78 @@ +cacher = new ApplicationCacher($this->app['cache']->store('array'), []); + } + + #[Test] + public function the_shared_error_is_scoped_to_the_current_site() + { + $this->setSites([ + 'english' => ['url' => 'http://localhost/', 'locale' => 'en'], + 'french' => ['url' => 'http://localhost/fr/', 'locale' => 'fr'], + ]); + + // Render and share the english 404 first. + Site::setCurrent('english'); + $this->shareError(404, 'English not found'); + + // The french site must not be considered to already have a shared error. + // Before scoping by site, the english error would leak here and break the + // localization (e.g. the language picker) for every other site. + Site::setCurrent('french'); + $this->assertFalse($this->cacher->hasCachedPage($this->sharedErrorRequest(404))); + + $this->shareError(404, 'French not found'); + + // Each site now serves its own localized shared error. + Site::setCurrent('english'); + $this->assertTrue($this->cacher->hasCachedPage($this->sharedErrorRequest(404))); + $this->assertEquals('English not found', $this->cacher->getCachedPage($this->sharedErrorRequest(404))->content); + + Site::setCurrent('french'); + $this->assertTrue($this->cacher->hasCachedPage($this->sharedErrorRequest(404))); + $this->assertEquals('French not found', $this->cacher->getCachedPage($this->sharedErrorRequest(404))->content); + } + + /** + * Replicates the request that's built when a shared error is written + * (the Cache middleware's copyError) and read (RendersHttpExceptions's + * getCachedError) for the current site. + */ + private function sharedErrorRequest(int $status): Request + { + return Request::createFrom(Request::create('http://localhost/'))->fakeStaticCacheStatus($status); + } + + /** + * Cache a shared error for the current site, mirroring how the cacher + * persists pages once the response has been prepared. + */ + private function shareError(int $status, string $content): void + { + $request = $this->sharedErrorRequest($status); + $response = response($content, $status); + + $this->cacher->cachePage($request, $response); + + // The cacher writes to the store on the ResponsePrepared event, which + // the framework fires during the real response lifecycle. + event(new ResponsePrepared($request, $response)); + } +}