From 34b588013206c5d0c80c6daa1a850129074436dc Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Mon, 20 Jul 2026 11:51:15 -0400 Subject: [PATCH 01/11] Namespace origin cache purge tags --- src/StaticCache.php | 14 ++++++++++---- tests/unit/StaticCacheTest.php | 26 +++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/StaticCache.php b/src/StaticCache.php index 8104e46b..f9503b42 100644 --- a/src/StaticCache.php +++ b/src/StaticCache.php @@ -27,8 +27,10 @@ * The values are comma-separated and can be in several formats: * * - Added by the gateway: - * - `{environmentId}` - * - `{environmentId}:{uri}` (URI has a leading and no trailing slash) + * - `{environmentId}` (legacy) + * - `{environmentId}:{uri}` (legacy; URI has a leading and no trailing slash) + * - `origin:{environmentId}` + * - `origin:{environmentId}:{uri}` (URI has a leading and no trailing slash) * - Added by the CDN: * - `cdn:{environmentId}` * - `cdn:{environmentId}:{objectKey}` (object key has no leading slash) @@ -225,9 +227,12 @@ public function purgeAll(): void public function purgeOrigin(): void { + $environmentId = Module::getInstance()->getConfig()->environmentId; + // Purge the legacy unnamespaced tag alongside the origin tag. $tags = $this->beforePurge( null, - Module::getInstance()->getConfig()->environmentId, + $environmentId, + "origin:$environmentId", ); $this->tagsToPurge->push(...$tags); } @@ -266,7 +271,8 @@ private function purgeElementUri(ElementInterface $element): bool : Path::new($uri)->withLeadingSlash()->withoutTrailingSlash(); $environmentId = Module::getInstance()->getConfig()->environmentId; - $tags = $this->beforePurge($element, "$environmentId:$uri"); + // Purge the legacy unnamespaced URI tag alongside the origin tag. + $tags = $this->beforePurge($element, "$environmentId:$uri", "origin:$environmentId:$uri"); $this->tagsToPurge = $tags->concat($this->tagsToPurge); return $tags->isNotEmpty(); diff --git a/tests/unit/StaticCacheTest.php b/tests/unit/StaticCacheTest.php index ce87696d..d91987b7 100644 --- a/tests/unit/StaticCacheTest.php +++ b/tests/unit/StaticCacheTest.php @@ -218,6 +218,20 @@ public function testPurgeTagsKeepExistingHeaderTags(): void ); } + public function testPurgeAllUsesOriginAndCdnTags(): void + { + $staticCache = new StaticCache(); + + $staticCache->purgeAll(); + + $this->assertSame( + ['123-environment-id', 'origin:123-environment-id', 'cdn:123-environment-id'], + $this->collectionProperty($staticCache, 'tagsToPurge') + ->map(fn(StaticCacheTag $tag) => $tag->getValue()) + ->all(), + ); + } + public function testBeforePurgeEventCanCancelPurge(): void { $staticCache = new StaticCache(); @@ -266,7 +280,7 @@ public function testFailedFetchRequestFallsBackToPurgeHeader(): void } $this->assertSame( - '123-environment-id:/news', + '123-environment-id:/news,origin:123-environment-id:/news', Craft::$app->getResponse()->getHeaders()->get(HeaderEnum::CACHE_PURGE_TAG->value), ); } @@ -313,7 +327,10 @@ public function testBeforePurgeEventReceivesElement(): void $this->saveElement($staticCache, $element); $this->assertSame($element, $purgeEvent->element); - $this->assertSame('123-environment-id:/news', $purgeEvent->tags[0]->getValue()); + $this->assertSame( + ['123-environment-id:/news', 'origin:123-environment-id:/news'], + array_map(fn(StaticCacheTag $tag) => $tag->getValue(), $purgeEvent->tags), + ); } public function testCancelledElementPurgeDoesNotQueueFetch(): void @@ -369,7 +386,10 @@ public function testSavedElementPurgeRequestIncludesFetchUrls(): void flags: JSON_THROW_ON_ERROR, ); - $this->assertSame(['123-environment-id:/news'], $payload['tags']); + $this->assertSame([ + '123-environment-id:/news', + 'origin:123-environment-id:/news', + ], $payload['tags']); $this->assertSame([ 'https://example.com/news', 'https://example.com/fr/nouvelles', From 17aba27d2074616e1cd648e275d5a72d70fd3571 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Mon, 20 Jul 2026 12:36:32 -0400 Subject: [PATCH 02/11] Preserve legacy homepage purge tag --- src/StaticCache.php | 10 ++++++---- tests/unit/StaticCacheTest.php | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/StaticCache.php b/src/StaticCache.php index f9503b42..0dbb313f 100644 --- a/src/StaticCache.php +++ b/src/StaticCache.php @@ -28,7 +28,7 @@ * * - Added by the gateway: * - `{environmentId}` (legacy) - * - `{environmentId}:{uri}` (legacy; URI has a leading and no trailing slash) + * - `{environmentId}:{uri}` (legacy; homepage URI is empty, otherwise leading with no trailing slash) * - `origin:{environmentId}` * - `origin:{environmentId}:{uri}` (URI has a leading and no trailing slash) * - Added by the CDN: @@ -266,13 +266,15 @@ private function purgeElementUri(ElementInterface $element): bool return false; } - $uri = $element->getIsHomepage() + $isHomepage = $element->getIsHomepage(); + $uri = $isHomepage ? '/' : Path::new($uri)->withLeadingSlash()->withoutTrailingSlash(); $environmentId = Module::getInstance()->getConfig()->environmentId; - // Purge the legacy unnamespaced URI tag alongside the origin tag. - $tags = $this->beforePurge($element, "$environmentId:$uri", "origin:$environmentId:$uri"); + // Legacy URI tags used an empty homepage URI. + $legacyTag = $isHomepage ? "$environmentId:" : "$environmentId:$uri"; + $tags = $this->beforePurge($element, $legacyTag, "origin:$environmentId:$uri"); $this->tagsToPurge = $tags->concat($this->tagsToPurge); return $tags->isNotEmpty(); diff --git a/tests/unit/StaticCacheTest.php b/tests/unit/StaticCacheTest.php index d91987b7..59504d6a 100644 --- a/tests/unit/StaticCacheTest.php +++ b/tests/unit/StaticCacheTest.php @@ -333,6 +333,21 @@ public function testBeforePurgeEventReceivesElement(): void ); } + public function testHomepagePurgeUsesLegacyEmptyUriTag(): void + { + $staticCache = new StaticCache(); + $element = new Entry(['uri' => Entry::HOMEPAGE_URI]); + + $this->saveElement($staticCache, $element); + + $this->assertSame( + ['123-environment-id:', 'origin:123-environment-id:/'], + $this->collectionProperty($staticCache, 'tagsToPurge') + ->map(fn(StaticCacheTag $tag) => $tag->getValue()) + ->all(), + ); + } + public function testCancelledElementPurgeDoesNotQueueFetch(): void { $staticCache = new StaticCache(); From b9d4682b843a364caebefe28ee3cddbcb8128ce8 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Mon, 20 Jul 2026 12:41:32 -0400 Subject: [PATCH 03/11] Clarify homepage origin tag docs --- src/StaticCache.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticCache.php b/src/StaticCache.php index 0dbb313f..b869e001 100644 --- a/src/StaticCache.php +++ b/src/StaticCache.php @@ -30,7 +30,7 @@ * - `{environmentId}` (legacy) * - `{environmentId}:{uri}` (legacy; homepage URI is empty, otherwise leading with no trailing slash) * - `origin:{environmentId}` - * - `origin:{environmentId}:{uri}` (URI has a leading and no trailing slash) + * - `origin:{environmentId}:{uri}` (homepage URI is `/`, otherwise leading with no trailing slash) * - Added by the CDN: * - `cdn:{environmentId}` * - `cdn:{environmentId}:{objectKey}` (object key has no leading slash) From f63f543dba76c8e7bc01cb8bde4bdac49ea67443 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Mon, 20 Jul 2026 12:44:38 -0400 Subject: [PATCH 04/11] Polish cache tag documentation --- src/StaticCache.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StaticCache.php b/src/StaticCache.php index b869e001..faf8c715 100644 --- a/src/StaticCache.php +++ b/src/StaticCache.php @@ -28,9 +28,9 @@ * * - Added by the gateway: * - `{environmentId}` (legacy) - * - `{environmentId}:{uri}` (legacy; homepage URI is empty, otherwise leading with no trailing slash) + * - `{environmentId}:{uri}` (legacy; homepage URI is empty, otherwise with a leading and no trailing slash) * - `origin:{environmentId}` - * - `origin:{environmentId}:{uri}` (homepage URI is `/`, otherwise leading with no trailing slash) + * - `origin:{environmentId}:{uri}` (homepage URI is `/`, otherwise with a leading and no trailing slash) * - Added by the CDN: * - `cdn:{environmentId}` * - `cdn:{environmentId}:{objectKey}` (object key has no leading slash) From c784e36bf5c9cf9d2b24871369d17d09582d8f20 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Mon, 20 Jul 2026 13:22:06 -0400 Subject: [PATCH 05/11] Drop legacy homepage purge tags --- src/StaticCache.php | 10 ++++++---- tests/unit/StaticCacheTest.php | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/StaticCache.php b/src/StaticCache.php index faf8c715..67d63e0d 100644 --- a/src/StaticCache.php +++ b/src/StaticCache.php @@ -28,7 +28,7 @@ * * - Added by the gateway: * - `{environmentId}` (legacy) - * - `{environmentId}:{uri}` (legacy; homepage URI is empty, otherwise with a leading and no trailing slash) + * - `{environmentId}:{uri}` (legacy; non-homepage URI has a leading and no trailing slash) * - `origin:{environmentId}` * - `origin:{environmentId}:{uri}` (homepage URI is `/`, otherwise with a leading and no trailing slash) * - Added by the CDN: @@ -272,9 +272,11 @@ private function purgeElementUri(ElementInterface $element): bool : Path::new($uri)->withLeadingSlash()->withoutTrailingSlash(); $environmentId = Module::getInstance()->getConfig()->environmentId; - // Legacy URI tags used an empty homepage URI. - $legacyTag = $isHomepage ? "$environmentId:" : "$environmentId:$uri"; - $tags = $this->beforePurge($element, $legacyTag, "origin:$environmentId:$uri"); + // Keep the legacy unnamespaced tag for non-homepage URIs during rollout. + $tagValues = $isHomepage + ? ["origin:$environmentId:$uri"] + : ["$environmentId:$uri", "origin:$environmentId:$uri"]; + $tags = $this->beforePurge($element, ...$tagValues); $this->tagsToPurge = $tags->concat($this->tagsToPurge); return $tags->isNotEmpty(); diff --git a/tests/unit/StaticCacheTest.php b/tests/unit/StaticCacheTest.php index 59504d6a..67d04605 100644 --- a/tests/unit/StaticCacheTest.php +++ b/tests/unit/StaticCacheTest.php @@ -333,7 +333,7 @@ public function testBeforePurgeEventReceivesElement(): void ); } - public function testHomepagePurgeUsesLegacyEmptyUriTag(): void + public function testHomepagePurgeUsesOriginTag(): void { $staticCache = new StaticCache(); $element = new Entry(['uri' => Entry::HOMEPAGE_URI]); @@ -341,7 +341,7 @@ public function testHomepagePurgeUsesLegacyEmptyUriTag(): void $this->saveElement($staticCache, $element); $this->assertSame( - ['123-environment-id:', 'origin:123-environment-id:/'], + ['origin:123-environment-id:/'], $this->collectionProperty($staticCache, 'tagsToPurge') ->map(fn(StaticCacheTag $tag) => $tag->getValue()) ->all(), From 1bd57e872f9f6b715c6eb6878a0564dfc3b0cc32 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Mon, 20 Jul 2026 13:41:23 -0400 Subject: [PATCH 06/11] Namespace cache tags by environment --- src/StaticCache.php | 23 ++++++++++++++--------- src/fs/Fs.php | 13 +++++++------ tests/unit/StaticCacheTest.php | 27 +++++++++++++++++++++------ 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/StaticCache.php b/src/StaticCache.php index 67d63e0d..1d8014f2 100644 --- a/src/StaticCache.php +++ b/src/StaticCache.php @@ -29,11 +29,13 @@ * - Added by the gateway: * - `{environmentId}` (legacy) * - `{environmentId}:{uri}` (legacy; non-homepage URI has a leading and no trailing slash) - * - `origin:{environmentId}` - * - `origin:{environmentId}:{uri}` (homepage URI is `/`, otherwise with a leading and no trailing slash) + * - `{environmentId}:uri` + * - `{environmentId}:uri:{uri}` (homepage URI is `/`, otherwise with a leading and no trailing slash) * - Added by the CDN: - * - `cdn:{environmentId}` - * - `cdn:{environmentId}:{objectKey}` (object key has no leading slash) + * - `cdn:{environmentId}` (legacy) + * - `cdn:{environmentId}:{objectKey}` (legacy; object key has no leading slash) + * - `{environmentId}:cdn` + * - `{environmentId}:cdn:{objectKey}` (object key has no leading slash) * - Added by Craft: * - `{environmentShortId}{hashed}` * - `{environmentId}:overflow` (when the response has too many cache tags) @@ -228,11 +230,11 @@ public function purgeAll(): void public function purgeOrigin(): void { $environmentId = Module::getInstance()->getConfig()->environmentId; - // Purge the legacy unnamespaced tag alongside the origin tag. + // Purge the legacy unscoped tag alongside the URI-family tag. $tags = $this->beforePurge( null, $environmentId, - "origin:$environmentId", + "$environmentId:uri", ); $this->tagsToPurge->push(...$tags); } @@ -251,9 +253,12 @@ public function purgeGateway(): void public function purgeCdn(): void { + $environmentId = Module::getInstance()->getConfig()->environmentId; + // Purge the legacy cache-family-first tag alongside the environment-first tag. $tags = $this->beforePurge( null, - self::CDN_PREFIX . Module::getInstance()->getConfig()->environmentId, + self::CDN_PREFIX . $environmentId, + "$environmentId:cdn", ); $this->tagsToPurge->push(...$tags); } @@ -274,8 +279,8 @@ private function purgeElementUri(ElementInterface $element): bool $environmentId = Module::getInstance()->getConfig()->environmentId; // Keep the legacy unnamespaced tag for non-homepage URIs during rollout. $tagValues = $isHomepage - ? ["origin:$environmentId:$uri"] - : ["$environmentId:$uri", "origin:$environmentId:$uri"]; + ? ["$environmentId:uri:$uri"] + : ["$environmentId:$uri", "$environmentId:uri:$uri"]; $tags = $this->beforePurge($element, ...$tagValues); $this->tagsToPurge = $tags->concat($this->tagsToPurge); diff --git a/src/fs/Fs.php b/src/fs/Fs.php index 99e75ea0..15fd68bc 100644 --- a/src/fs/Fs.php +++ b/src/fs/Fs.php @@ -9,7 +9,6 @@ use craft\behaviors\EnvAttributeParserBehavior; use craft\cloud\Module; use craft\cloud\StaticCache; -use craft\cloud\StaticCacheTag; use craft\elements\Asset; use craft\errors\FsException; use craft\flysystem\base\FlysystemFs; @@ -202,12 +201,14 @@ protected function createAdapter(): AwsS3V3Adapter protected function invalidateCdnPath(string $path): bool { try { - $prefix = StaticCache::CDN_PREFIX . Module::getInstance()->getConfig()->environmentId . ':'; - $tag = StaticCacheTag::create($this->createBucketPath($path)->toString()) - ->minify(false) - ->withPrefix($prefix); + $environmentId = Module::getInstance()->getConfig()->environmentId; + $objectKey = $this->createBucketPath($path)->toString(); - Module::getInstance()->getStaticCache()->purgeTags($tag); + // Purge the legacy cache-family-first tag alongside the environment-first tag. + Module::getInstance()->getStaticCache()->purgeTags( + StaticCache::CDN_PREFIX . "$environmentId:$objectKey", + "$environmentId:cdn:$objectKey", + ); return true; } catch (\Throwable $e) { diff --git a/tests/unit/StaticCacheTest.php b/tests/unit/StaticCacheTest.php index 67d04605..98b1af75 100644 --- a/tests/unit/StaticCacheTest.php +++ b/tests/unit/StaticCacheTest.php @@ -5,6 +5,7 @@ use Codeception\Test\Unit; use Craft; use craft\cloud\events\PurgeEvent; +use craft\cloud\fs\AssetsFs; use craft\cloud\HeaderEnum; use craft\cloud\Module; use craft\cloud\signing\RequestSigner; @@ -225,13 +226,27 @@ public function testPurgeAllUsesOriginAndCdnTags(): void $staticCache->purgeAll(); $this->assertSame( - ['123-environment-id', 'origin:123-environment-id', 'cdn:123-environment-id'], + ['123-environment-id', '123-environment-id:uri', 'cdn:123-environment-id', '123-environment-id:cdn'], $this->collectionProperty($staticCache, 'tagsToPurge') ->map(fn(StaticCacheTag $tag) => $tag->getValue()) ->all(), ); } + public function testAssetCdnPurgeUsesLegacyAndEnvironmentFirstTags(): void + { + $staticCache = new StaticCache(); + Module::getInstance()->set('staticCache', $staticCache); + $fs = new AssetsFs(); + $method = new ReflectionMethod($fs, 'invalidateCdnPath'); + + $this->assertTrue($method->invoke($fs, 'image.jpg')); + $this->assertSame( + 'cdn:123-environment-id:123-environment-id/assets/image.jpg,123-environment-id:cdn:123-environment-id/assets/image.jpg', + Craft::$app->getResponse()->getHeaders()->get(HeaderEnum::CACHE_PURGE_TAG->value), + ); + } + public function testBeforePurgeEventCanCancelPurge(): void { $staticCache = new StaticCache(); @@ -280,7 +295,7 @@ public function testFailedFetchRequestFallsBackToPurgeHeader(): void } $this->assertSame( - '123-environment-id:/news,origin:123-environment-id:/news', + '123-environment-id:/news,123-environment-id:uri:/news', Craft::$app->getResponse()->getHeaders()->get(HeaderEnum::CACHE_PURGE_TAG->value), ); } @@ -328,12 +343,12 @@ public function testBeforePurgeEventReceivesElement(): void $this->assertSame($element, $purgeEvent->element); $this->assertSame( - ['123-environment-id:/news', 'origin:123-environment-id:/news'], + ['123-environment-id:/news', '123-environment-id:uri:/news'], array_map(fn(StaticCacheTag $tag) => $tag->getValue(), $purgeEvent->tags), ); } - public function testHomepagePurgeUsesOriginTag(): void + public function testHomepagePurgeUsesUriTag(): void { $staticCache = new StaticCache(); $element = new Entry(['uri' => Entry::HOMEPAGE_URI]); @@ -341,7 +356,7 @@ public function testHomepagePurgeUsesOriginTag(): void $this->saveElement($staticCache, $element); $this->assertSame( - ['origin:123-environment-id:/'], + ['123-environment-id:uri:/'], $this->collectionProperty($staticCache, 'tagsToPurge') ->map(fn(StaticCacheTag $tag) => $tag->getValue()) ->all(), @@ -403,7 +418,7 @@ public function testSavedElementPurgeRequestIncludesFetchUrls(): void $this->assertSame([ '123-environment-id:/news', - 'origin:123-environment-id:/news', + '123-environment-id:uri:/news', ], $payload['tags']); $this->assertSame([ 'https://example.com/news', From ddaf78410238f66e95e2c1a9859c711d7cb1db68 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Mon, 20 Jul 2026 13:44:35 -0400 Subject: [PATCH 07/11] Prepare reflected CDN purge method --- tests/unit/StaticCacheTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/StaticCacheTest.php b/tests/unit/StaticCacheTest.php index 98b1af75..cd036638 100644 --- a/tests/unit/StaticCacheTest.php +++ b/tests/unit/StaticCacheTest.php @@ -239,6 +239,7 @@ public function testAssetCdnPurgeUsesLegacyAndEnvironmentFirstTags(): void Module::getInstance()->set('staticCache', $staticCache); $fs = new AssetsFs(); $method = new ReflectionMethod($fs, 'invalidateCdnPath'); + $method->setAccessible(true); $this->assertTrue($method->invoke($fs, 'image.jpg')); $this->assertSame( From 6dc0e83dba6648f73c941a9bfe8b1b46c32908ce Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Mon, 20 Jul 2026 15:31:40 -0400 Subject: [PATCH 08/11] Document family overflow cache tags --- src/StaticCache.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/StaticCache.php b/src/StaticCache.php index 1d8014f2..98a7c5ad 100644 --- a/src/StaticCache.php +++ b/src/StaticCache.php @@ -31,11 +31,13 @@ * - `{environmentId}:{uri}` (legacy; non-homepage URI has a leading and no trailing slash) * - `{environmentId}:uri` * - `{environmentId}:uri:{uri}` (homepage URI is `/`, otherwise with a leading and no trailing slash) + * - `{environmentId}:uri:overflow` * - Added by the CDN: * - `cdn:{environmentId}` (legacy) * - `cdn:{environmentId}:{objectKey}` (legacy; object key has no leading slash) * - `{environmentId}:cdn` * - `{environmentId}:cdn:{objectKey}` (object key has no leading slash) + * - `{environmentId}:cdn:overflow` * - Added by Craft: * - `{environmentShortId}{hashed}` * - `{environmentId}:overflow` (when the response has too many cache tags) From 4178a0854c1c2430783f31128325f980194a7ef5 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Mon, 20 Jul 2026 15:37:16 -0400 Subject: [PATCH 09/11] Handle overlong cache purge selectors --- src/StaticCache.php | 21 ++++++++++++++------- src/fs/Fs.php | 7 +++++-- tests/unit/StaticCacheTest.php | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/StaticCache.php b/src/StaticCache.php index 98a7c5ad..89d1fdc8 100644 --- a/src/StaticCache.php +++ b/src/StaticCache.php @@ -31,16 +31,16 @@ * - `{environmentId}:{uri}` (legacy; non-homepage URI has a leading and no trailing slash) * - `{environmentId}:uri` * - `{environmentId}:uri:{uri}` (homepage URI is `/`, otherwise with a leading and no trailing slash) - * - `{environmentId}:uri:overflow` + * - `{environmentId}:uri:overflow` (when the URI selector is too long) * - Added by the CDN: * - `cdn:{environmentId}` (legacy) * - `cdn:{environmentId}:{objectKey}` (legacy; object key has no leading slash) * - `{environmentId}:cdn` * - `{environmentId}:cdn:{objectKey}` (object key has no leading slash) - * - `{environmentId}:cdn:overflow` + * - `{environmentId}:cdn:overflow` (when the object key selector is too long) * - Added by Craft: * - `{environmentShortId}{hashed}` - * - `{environmentId}:overflow` (when the response has too many cache tags) + * - `{environmentId}:overflow` (shared overflow and legacy CDN selector fallback) */ class StaticCache extends \yii\base\Component { @@ -57,7 +57,7 @@ class StaticCache extends \yii\base\Component * @see https://developers.cloudflare.com/workers/cache/configuration/ */ private const MAX_TAG_HEADER_VALUE_LENGTH = 16 * 1024; - private const MAX_TAG_VALUE_LENGTH = 1024; + public const MAX_TAG_VALUE_LENGTH = 1024; private const MAX_TAG_COUNT = 1000; private ?int $cacheDuration = null; private Collection $tags; @@ -281,9 +281,16 @@ private function purgeElementUri(ElementInterface $element): bool $environmentId = Module::getInstance()->getConfig()->environmentId; // Keep the legacy unnamespaced tag for non-homepage URIs during rollout. $tagValues = $isHomepage - ? ["$environmentId:uri:$uri"] - : ["$environmentId:$uri", "$environmentId:uri:$uri"]; - $tags = $this->beforePurge($element, ...$tagValues); + ? [StaticCacheTag::create("$environmentId:uri:$uri")] + : [StaticCacheTag::create("$environmentId:$uri"), StaticCacheTag::create("$environmentId:uri:$uri")]; + $overflowTag = StaticCacheTag::create("$environmentId:uri:overflow"); + $tags = $this->beforePurge( + $element, + ...array_map( + fn(StaticCacheTag $tag) => strlen($tag->getValue()) > self::MAX_TAG_VALUE_LENGTH ? $overflowTag : $tag, + $tagValues, + ), + ); $this->tagsToPurge = $tags->concat($this->tagsToPurge); return $tags->isNotEmpty(); diff --git a/src/fs/Fs.php b/src/fs/Fs.php index 15fd68bc..40d7ad07 100644 --- a/src/fs/Fs.php +++ b/src/fs/Fs.php @@ -9,6 +9,7 @@ use craft\behaviors\EnvAttributeParserBehavior; use craft\cloud\Module; use craft\cloud\StaticCache; +use craft\cloud\StaticCacheTag; use craft\elements\Asset; use craft\errors\FsException; use craft\flysystem\base\FlysystemFs; @@ -203,11 +204,13 @@ protected function invalidateCdnPath(string $path): bool try { $environmentId = Module::getInstance()->getConfig()->environmentId; $objectKey = $this->createBucketPath($path)->toString(); + $legacyTag = StaticCacheTag::create(StaticCache::CDN_PREFIX . "$environmentId:$objectKey"); + $cdnTag = StaticCacheTag::create("$environmentId:cdn:$objectKey"); // Purge the legacy cache-family-first tag alongside the environment-first tag. Module::getInstance()->getStaticCache()->purgeTags( - StaticCache::CDN_PREFIX . "$environmentId:$objectKey", - "$environmentId:cdn:$objectKey", + strlen($legacyTag->getValue()) > StaticCache::MAX_TAG_VALUE_LENGTH ? "$environmentId:overflow" : $legacyTag, + strlen($cdnTag->getValue()) > StaticCache::MAX_TAG_VALUE_LENGTH ? "$environmentId:cdn:overflow" : $cdnTag, ); return true; diff --git a/tests/unit/StaticCacheTest.php b/tests/unit/StaticCacheTest.php index cd036638..ab16598f 100644 --- a/tests/unit/StaticCacheTest.php +++ b/tests/unit/StaticCacheTest.php @@ -248,6 +248,21 @@ public function testAssetCdnPurgeUsesLegacyAndEnvironmentFirstTags(): void ); } + public function testOverlongAssetCdnPurgeUsesOverflowTags(): void + { + $staticCache = new StaticCache(); + Module::getInstance()->set('staticCache', $staticCache); + $fs = new AssetsFs(); + $method = new ReflectionMethod($fs, 'invalidateCdnPath'); + $method->setAccessible(true); + + $this->assertTrue($method->invoke($fs, str_repeat('x', 1024))); + $this->assertSame( + '123-environment-id:overflow,123-environment-id:cdn:overflow', + Craft::$app->getResponse()->getHeaders()->get(HeaderEnum::CACHE_PURGE_TAG->value), + ); + } + public function testBeforePurgeEventCanCancelPurge(): void { $staticCache = new StaticCache(); @@ -364,6 +379,24 @@ public function testHomepagePurgeUsesUriTag(): void ); } + public function testOverlongElementUriPurgeUsesOverflowTag(): void + { + $staticCache = new StaticCache(); + $element = new FetchableElement(['uri' => str_repeat('x', 1024)]); + $element->fetchUrl = 'https://example.com/overlong'; + + $this->saveElement($staticCache, $element); + $this->sendPendingPurgeTags($staticCache); + + $payload = json_decode( + (string) $this->gatewayRequest?->getBody(), + true, + flags: JSON_THROW_ON_ERROR, + ); + + $this->assertSame(['123-environment-id:uri:overflow'], $payload['tags']); + } + public function testCancelledElementPurgeDoesNotQueueFetch(): void { $staticCache = new StaticCache(); From 858189af3c5c440aa79f473e3b7241512051ec64 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Mon, 20 Jul 2026 20:56:03 -0400 Subject: [PATCH 10/11] Use shared cache overflow tag --- src/StaticCache.php | 6 ++---- src/fs/Fs.php | 2 +- tests/unit/StaticCacheTest.php | 6 +++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/StaticCache.php b/src/StaticCache.php index 89d1fdc8..9f1d8e4e 100644 --- a/src/StaticCache.php +++ b/src/StaticCache.php @@ -31,16 +31,14 @@ * - `{environmentId}:{uri}` (legacy; non-homepage URI has a leading and no trailing slash) * - `{environmentId}:uri` * - `{environmentId}:uri:{uri}` (homepage URI is `/`, otherwise with a leading and no trailing slash) - * - `{environmentId}:uri:overflow` (when the URI selector is too long) * - Added by the CDN: * - `cdn:{environmentId}` (legacy) * - `cdn:{environmentId}:{objectKey}` (legacy; object key has no leading slash) * - `{environmentId}:cdn` * - `{environmentId}:cdn:{objectKey}` (object key has no leading slash) - * - `{environmentId}:cdn:overflow` (when the object key selector is too long) * - Added by Craft: * - `{environmentShortId}{hashed}` - * - `{environmentId}:overflow` (shared overflow and legacy CDN selector fallback) + * - `{environmentId}:overflow` (when the response has too many tags or a selector is too long) */ class StaticCache extends \yii\base\Component { @@ -283,7 +281,7 @@ private function purgeElementUri(ElementInterface $element): bool $tagValues = $isHomepage ? [StaticCacheTag::create("$environmentId:uri:$uri")] : [StaticCacheTag::create("$environmentId:$uri"), StaticCacheTag::create("$environmentId:uri:$uri")]; - $overflowTag = StaticCacheTag::create("$environmentId:uri:overflow"); + $overflowTag = $this->overflowTag(); $tags = $this->beforePurge( $element, ...array_map( diff --git a/src/fs/Fs.php b/src/fs/Fs.php index 40d7ad07..8de787b4 100644 --- a/src/fs/Fs.php +++ b/src/fs/Fs.php @@ -210,7 +210,7 @@ protected function invalidateCdnPath(string $path): bool // Purge the legacy cache-family-first tag alongside the environment-first tag. Module::getInstance()->getStaticCache()->purgeTags( strlen($legacyTag->getValue()) > StaticCache::MAX_TAG_VALUE_LENGTH ? "$environmentId:overflow" : $legacyTag, - strlen($cdnTag->getValue()) > StaticCache::MAX_TAG_VALUE_LENGTH ? "$environmentId:cdn:overflow" : $cdnTag, + strlen($cdnTag->getValue()) > StaticCache::MAX_TAG_VALUE_LENGTH ? "$environmentId:overflow" : $cdnTag, ); return true; diff --git a/tests/unit/StaticCacheTest.php b/tests/unit/StaticCacheTest.php index ab16598f..9688039a 100644 --- a/tests/unit/StaticCacheTest.php +++ b/tests/unit/StaticCacheTest.php @@ -248,7 +248,7 @@ public function testAssetCdnPurgeUsesLegacyAndEnvironmentFirstTags(): void ); } - public function testOverlongAssetCdnPurgeUsesOverflowTags(): void + public function testOverlongAssetCdnPurgeUsesOverflowTag(): void { $staticCache = new StaticCache(); Module::getInstance()->set('staticCache', $staticCache); @@ -258,7 +258,7 @@ public function testOverlongAssetCdnPurgeUsesOverflowTags(): void $this->assertTrue($method->invoke($fs, str_repeat('x', 1024))); $this->assertSame( - '123-environment-id:overflow,123-environment-id:cdn:overflow', + '123-environment-id:overflow', Craft::$app->getResponse()->getHeaders()->get(HeaderEnum::CACHE_PURGE_TAG->value), ); } @@ -394,7 +394,7 @@ public function testOverlongElementUriPurgeUsesOverflowTag(): void flags: JSON_THROW_ON_ERROR, ); - $this->assertSame(['123-environment-id:uri:overflow'], $payload['tags']); + $this->assertSame(['123-environment-id:overflow'], $payload['tags']); } public function testCancelledElementPurgeDoesNotQueueFetch(): void From ebc8e7ea74cb0f7e9e17583a61901a4126e60dc6 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Mon, 20 Jul 2026 21:12:10 -0400 Subject: [PATCH 11/11] Remove legacy package purge tags --- src/StaticCache.php | 19 +++---------------- src/fs/Fs.php | 3 --- tests/unit/StaticCacheTest.php | 15 ++++++--------- 3 files changed, 9 insertions(+), 28 deletions(-) diff --git a/src/StaticCache.php b/src/StaticCache.php index 9f1d8e4e..640fb387 100644 --- a/src/StaticCache.php +++ b/src/StaticCache.php @@ -42,8 +42,6 @@ */ class StaticCache extends \yii\base\Component { - public const CDN_PREFIX = 'cdn:'; - /** * @event PurgeEvent The event that is triggered before static cache tags are purged. */ @@ -230,10 +228,8 @@ public function purgeAll(): void public function purgeOrigin(): void { $environmentId = Module::getInstance()->getConfig()->environmentId; - // Purge the legacy unscoped tag alongside the URI-family tag. $tags = $this->beforePurge( null, - $environmentId, "$environmentId:uri", ); $this->tagsToPurge->push(...$tags); @@ -254,10 +250,8 @@ public function purgeGateway(): void public function purgeCdn(): void { $environmentId = Module::getInstance()->getConfig()->environmentId; - // Purge the legacy cache-family-first tag alongside the environment-first tag. $tags = $this->beforePurge( null, - self::CDN_PREFIX . $environmentId, "$environmentId:cdn", ); $this->tagsToPurge->push(...$tags); @@ -271,23 +265,16 @@ private function purgeElementUri(ElementInterface $element): bool return false; } - $isHomepage = $element->getIsHomepage(); - $uri = $isHomepage + $uri = $element->getIsHomepage() ? '/' : Path::new($uri)->withLeadingSlash()->withoutTrailingSlash(); $environmentId = Module::getInstance()->getConfig()->environmentId; - // Keep the legacy unnamespaced tag for non-homepage URIs during rollout. - $tagValues = $isHomepage - ? [StaticCacheTag::create("$environmentId:uri:$uri")] - : [StaticCacheTag::create("$environmentId:$uri"), StaticCacheTag::create("$environmentId:uri:$uri")]; + $tag = StaticCacheTag::create("$environmentId:uri:$uri"); $overflowTag = $this->overflowTag(); $tags = $this->beforePurge( $element, - ...array_map( - fn(StaticCacheTag $tag) => strlen($tag->getValue()) > self::MAX_TAG_VALUE_LENGTH ? $overflowTag : $tag, - $tagValues, - ), + strlen($tag->getValue()) > self::MAX_TAG_VALUE_LENGTH ? $overflowTag : $tag, ); $this->tagsToPurge = $tags->concat($this->tagsToPurge); diff --git a/src/fs/Fs.php b/src/fs/Fs.php index 8de787b4..eb06ac16 100644 --- a/src/fs/Fs.php +++ b/src/fs/Fs.php @@ -204,12 +204,9 @@ protected function invalidateCdnPath(string $path): bool try { $environmentId = Module::getInstance()->getConfig()->environmentId; $objectKey = $this->createBucketPath($path)->toString(); - $legacyTag = StaticCacheTag::create(StaticCache::CDN_PREFIX . "$environmentId:$objectKey"); $cdnTag = StaticCacheTag::create("$environmentId:cdn:$objectKey"); - // Purge the legacy cache-family-first tag alongside the environment-first tag. Module::getInstance()->getStaticCache()->purgeTags( - strlen($legacyTag->getValue()) > StaticCache::MAX_TAG_VALUE_LENGTH ? "$environmentId:overflow" : $legacyTag, strlen($cdnTag->getValue()) > StaticCache::MAX_TAG_VALUE_LENGTH ? "$environmentId:overflow" : $cdnTag, ); diff --git a/tests/unit/StaticCacheTest.php b/tests/unit/StaticCacheTest.php index 9688039a..ac0e6148 100644 --- a/tests/unit/StaticCacheTest.php +++ b/tests/unit/StaticCacheTest.php @@ -226,14 +226,14 @@ public function testPurgeAllUsesOriginAndCdnTags(): void $staticCache->purgeAll(); $this->assertSame( - ['123-environment-id', '123-environment-id:uri', 'cdn:123-environment-id', '123-environment-id:cdn'], + ['123-environment-id:uri', '123-environment-id:cdn'], $this->collectionProperty($staticCache, 'tagsToPurge') ->map(fn(StaticCacheTag $tag) => $tag->getValue()) ->all(), ); } - public function testAssetCdnPurgeUsesLegacyAndEnvironmentFirstTags(): void + public function testAssetCdnPurgeUsesEnvironmentFirstTag(): void { $staticCache = new StaticCache(); Module::getInstance()->set('staticCache', $staticCache); @@ -243,7 +243,7 @@ public function testAssetCdnPurgeUsesLegacyAndEnvironmentFirstTags(): void $this->assertTrue($method->invoke($fs, 'image.jpg')); $this->assertSame( - 'cdn:123-environment-id:123-environment-id/assets/image.jpg,123-environment-id:cdn:123-environment-id/assets/image.jpg', + '123-environment-id:cdn:123-environment-id/assets/image.jpg', Craft::$app->getResponse()->getHeaders()->get(HeaderEnum::CACHE_PURGE_TAG->value), ); } @@ -311,7 +311,7 @@ public function testFailedFetchRequestFallsBackToPurgeHeader(): void } $this->assertSame( - '123-environment-id:/news,123-environment-id:uri:/news', + '123-environment-id:uri:/news', Craft::$app->getResponse()->getHeaders()->get(HeaderEnum::CACHE_PURGE_TAG->value), ); } @@ -359,7 +359,7 @@ public function testBeforePurgeEventReceivesElement(): void $this->assertSame($element, $purgeEvent->element); $this->assertSame( - ['123-environment-id:/news', '123-environment-id:uri:/news'], + ['123-environment-id:uri:/news'], array_map(fn(StaticCacheTag $tag) => $tag->getValue(), $purgeEvent->tags), ); } @@ -450,10 +450,7 @@ public function testSavedElementPurgeRequestIncludesFetchUrls(): void flags: JSON_THROW_ON_ERROR, ); - $this->assertSame([ - '123-environment-id:/news', - '123-environment-id:uri:/news', - ], $payload['tags']); + $this->assertSame(['123-environment-id:uri:/news'], $payload['tags']); $this->assertSame([ 'https://example.com/news', 'https://example.com/fr/nouvelles',