@cloudflare/pages-shared 0.11.32 → 0.11.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,9 +25,18 @@ type BodyEncoding = "manual" | "automatic";
25
25
  // Before serving a 404, we check the cache to see if we've served this asset recently
26
26
  // and if so, serve it from the cache instead of responding with a 404.
27
27
  // This gives a bit of a grace period between deployments for any clients browsing the old deployment.
28
- export const ASSET_PRESERVATION_CACHE = "assetPreservationCache";
28
+ export const ASSET_PRESERVATION_CACHE_V1 = "assetPreservationCache";
29
+ // V2 stores the content hash instead of the asset.
30
+ // TODO: Remove V1 once we've fully migrated to V2
31
+ export const ASSET_PRESERVATION_CACHE_V2 = "assetPreservationCacheV2";
29
32
  const CACHE_CONTROL_PRESERVATION = "public, s-maxage=604800"; // 1 week
30
33
 
34
+ /** The preservation cache should be periodically
35
+ * written to so that the age / expiration is reset.
36
+ * Note: Up to 12 hours of jitter added to this value.
37
+ */
38
+ export const CACHE_PRESERVATION_WRITE_FREQUENCY = 86_400; // 1 day
39
+
31
40
  export const CACHE_CONTROL_BROWSER = "public, max-age=0, must-revalidate"; // have the browser check in with the server to make sure its local cache is valid before using it
32
41
  export const REDIRECTS_VERSION = 1;
33
42
  export const HEADERS_VERSION = 2;
@@ -311,6 +320,10 @@ export async function generateHandler<
311
320
  }
312
321
  }
313
322
 
323
+ function isNullBodyStatus(status: number): boolean {
324
+ return [101, 204, 205, 304].includes(status);
325
+ }
326
+
314
327
  async function attachHeaders(response: Response) {
315
328
  const existingHeaders = new Headers(response.headers);
316
329
 
@@ -456,7 +469,7 @@ export async function generateHandler<
456
469
 
457
470
  // https://fetch.spec.whatwg.org/#null-body-status
458
471
  return new Response(
459
- [101, 204, 205, 304].includes(response.status) ? null : response.body,
472
+ isNullBodyStatus(response.status) ? null : response.body,
460
473
  {
461
474
  headers: headers,
462
475
  status: response.status,
@@ -532,32 +545,43 @@ export async function generateHandler<
532
545
  response.headers.set("x-robots-tag", "noindex");
533
546
  }
534
547
 
535
- if (options.preserve) {
536
- // https://fetch.spec.whatwg.org/#null-body-status
537
- const preservedResponse = new Response(
538
- [101, 204, 205, 304].includes(response.status)
539
- ? null
540
- : response.clone().body,
541
- response
542
- );
543
- preservedResponse.headers.set(
544
- "cache-control",
545
- CACHE_CONTROL_PRESERVATION
546
- );
547
- preservedResponse.headers.set("x-robots-tag", "noindex");
548
+ if (options.preserve && waitUntil && caches) {
549
+ waitUntil(
550
+ (async () => {
551
+ try {
552
+ const assetPreservationCacheV2 = await caches.open(
553
+ ASSET_PRESERVATION_CACHE_V2
554
+ );
548
555
 
549
- if (waitUntil && caches) {
550
- waitUntil(
551
- caches
552
- .open(ASSET_PRESERVATION_CACHE)
553
- .then((assetPreservationCache) =>
554
- assetPreservationCache.put(request.url, preservedResponse)
555
- )
556
- .catch((err) => {
557
- logError(err);
558
- })
559
- );
560
- }
556
+ // Check if the asset has changed since last written to cache
557
+ // or if the cached entry is getting too old and should have
558
+ // it's expiration reset.
559
+ const match = await assetPreservationCacheV2.match(request);
560
+ if (
561
+ !match ||
562
+ assetKey !== (await match.text()) ||
563
+ isPreservationCacheResponseExpiring(match)
564
+ ) {
565
+ // cache the asset key in the cache with all the headers.
566
+ // When we read it back, we'll re-fetch the body but use the
567
+ // cached headers.
568
+ const preservedResponse = new Response(assetKey, response);
569
+ preservedResponse.headers.set(
570
+ "cache-control",
571
+ CACHE_CONTROL_PRESERVATION
572
+ );
573
+ preservedResponse.headers.set("x-robots-tag", "noindex");
574
+
575
+ await assetPreservationCacheV2.put(
576
+ request.url,
577
+ preservedResponse
578
+ );
579
+ }
580
+ } catch (err) {
581
+ logError(err as Error);
582
+ }
583
+ })()
584
+ );
561
585
  }
562
586
 
563
587
  if (
@@ -585,15 +609,66 @@ export async function generateHandler<
585
609
 
586
610
  async function notFound(): Promise<Response> {
587
611
  if (caches) {
588
- const assetPreservationCache = await caches.open(
589
- ASSET_PRESERVATION_CACHE
590
- );
591
- const preservedResponse = await assetPreservationCache.match(request.url);
592
- if (preservedResponse) {
593
- if (setMetrics) setMetrics({ preservationCacheResult: "checked-hit" });
594
- return preservedResponse;
595
- } else {
596
- if (setMetrics) setMetrics({ preservationCacheResult: "checked-miss" });
612
+ try {
613
+ const assetPreservationCacheV2 = await caches.open(
614
+ ASSET_PRESERVATION_CACHE_V2
615
+ );
616
+ let preservedResponse = await assetPreservationCacheV2.match(
617
+ request.url
618
+ );
619
+
620
+ // Continue serving from V1 preservation cache for some time to
621
+ // prevent 404s during the migration to V2
622
+ const cutoffDate = new Date("2024-05-17");
623
+ if (!preservedResponse && Date.now() < cutoffDate.getTime()) {
624
+ const assetPreservationCacheV1 = await caches.open(
625
+ ASSET_PRESERVATION_CACHE_V1
626
+ );
627
+ preservedResponse = await assetPreservationCacheV1.match(request.url);
628
+ if (preservedResponse) {
629
+ // V1 cache contains full response bodies so we return it directly
630
+ if (setMetrics) {
631
+ setMetrics({ preservationCacheResult: "checked-hit" });
632
+ }
633
+ return preservedResponse;
634
+ }
635
+ }
636
+
637
+ // V2 cache only contains the asset key, rather than the asset body:
638
+ if (preservedResponse) {
639
+ if (setMetrics) {
640
+ setMetrics({ preservationCacheResult: "checked-hit" });
641
+ }
642
+ // Always read the asset key to prevent hanging responses
643
+ const assetKey = await preservedResponse.text();
644
+ if (isNullBodyStatus(preservedResponse.status)) {
645
+ // We know the asset hasn't changed, so use the cached headers.
646
+ return new Response(null, preservedResponse);
647
+ }
648
+ if (assetKey) {
649
+ const asset = await fetchAsset(assetKey);
650
+ if (asset) {
651
+ // We know the asset hasn't changed, so use the cached headers.
652
+ return new Response(asset.body, preservedResponse);
653
+ } else {
654
+ logError(
655
+ new Error(
656
+ `preservation cache contained assetKey that does not exist in storage: ${assetKey}`
657
+ )
658
+ );
659
+ }
660
+ } else {
661
+ logError(new Error(`cached response had no assetKey: ${assetKey}`));
662
+ }
663
+ } else {
664
+ if (setMetrics) {
665
+ setMetrics({ preservationCacheResult: "checked-miss" });
666
+ }
667
+ }
668
+ } catch (err) {
669
+ // Don't throw an error because preservation cache is best effort.
670
+ // But log it because we should be able to fetch the asset here.
671
+ logError(err as Error);
597
672
  }
598
673
  } else {
599
674
  if (setMetrics) setMetrics({ preservationCacheResult: "disabled" });
@@ -662,6 +737,26 @@ function isPreview(url: URL): boolean {
662
737
  return false;
663
738
  }
664
739
 
740
+ /** Checks if a response is older than CACHE_PRESERVATION_WRITE_FREQUENCY
741
+ * and should be written to cache again to reset it's expiration.
742
+ */
743
+ export function isPreservationCacheResponseExpiring(
744
+ response: Response
745
+ ): boolean {
746
+ const ageHeader = response.headers.get("age");
747
+ if (!ageHeader) return false;
748
+ try {
749
+ const age = parseInt(ageHeader);
750
+ // Add up to 12 hours of jitter to help prevent a
751
+ // thundering heard when a lot of assets expire at once.
752
+ const jitter = Math.floor(Math.random() * 43_200);
753
+ if (age > CACHE_PRESERVATION_WRITE_FREQUENCY + jitter) return true;
754
+ } catch {
755
+ return false;
756
+ }
757
+ return false;
758
+ }
759
+
665
760
  /**
666
761
  * Whether or not the passed in string looks like an HTML
667
762
  * Content-Type header
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/pages-shared",
3
- "version": "0.11.32",
3
+ "version": "0.11.34",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cloudflare/workers-sdk.git",
@@ -13,7 +13,7 @@
13
13
  "metadata-generator/**/*"
14
14
  ],
15
15
  "dependencies": {
16
- "miniflare": "3.20240419.0"
16
+ "miniflare": "3.20240419.1"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@miniflare/storage-memory": "^2.14.2",