@cloudflare/pages-shared 0.11.32 → 0.11.33
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.
- package/asset-server/handler.ts +138 -36
- package/package.json +1 -1
package/asset-server/handler.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
472
|
+
isNullBodyStatus(response.status) ? null : response.body,
|
|
460
473
|
{
|
|
461
474
|
headers: headers,
|
|
462
475
|
status: response.status,
|
|
@@ -467,6 +480,13 @@ export async function generateHandler<
|
|
|
467
480
|
|
|
468
481
|
return await attachHeaders(await generateResponse());
|
|
469
482
|
|
|
483
|
+
/** We have non-standard cache behavior, so strip out all headers but keep the method */
|
|
484
|
+
function getCacheKey(): Request {
|
|
485
|
+
return new Request(request.url, {
|
|
486
|
+
method: request.method,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
470
490
|
async function serveAsset(
|
|
471
491
|
servingAssetEntry: AssetEntry,
|
|
472
492
|
options = { preserve: true }
|
|
@@ -532,32 +552,43 @@ export async function generateHandler<
|
|
|
532
552
|
response.headers.set("x-robots-tag", "noindex");
|
|
533
553
|
}
|
|
534
554
|
|
|
535
|
-
if (options.preserve) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
);
|
|
543
|
-
preservedResponse.headers.set(
|
|
544
|
-
"cache-control",
|
|
545
|
-
CACHE_CONTROL_PRESERVATION
|
|
546
|
-
);
|
|
547
|
-
preservedResponse.headers.set("x-robots-tag", "noindex");
|
|
555
|
+
if (options.preserve && waitUntil && caches) {
|
|
556
|
+
waitUntil(
|
|
557
|
+
(async () => {
|
|
558
|
+
try {
|
|
559
|
+
const assetPreservationCacheV2 = await caches.open(
|
|
560
|
+
ASSET_PRESERVATION_CACHE_V2
|
|
561
|
+
);
|
|
548
562
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
.
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
563
|
+
// Check if the asset has changed since last written to cache
|
|
564
|
+
// or if the cached entry is getting too old and should have
|
|
565
|
+
// it's expiration reset.
|
|
566
|
+
const match = await assetPreservationCacheV2.match(request);
|
|
567
|
+
if (
|
|
568
|
+
!match ||
|
|
569
|
+
assetKey !== (await match.text()) ||
|
|
570
|
+
isPreservationCacheResponseExpiring(match)
|
|
571
|
+
) {
|
|
572
|
+
// cache the asset key in the cache with all the headers.
|
|
573
|
+
// When we read it back, we'll re-fetch the body but use the
|
|
574
|
+
// cached headers.
|
|
575
|
+
const preservedResponse = new Response(assetKey, response);
|
|
576
|
+
preservedResponse.headers.set(
|
|
577
|
+
"cache-control",
|
|
578
|
+
CACHE_CONTROL_PRESERVATION
|
|
579
|
+
);
|
|
580
|
+
preservedResponse.headers.set("x-robots-tag", "noindex");
|
|
581
|
+
|
|
582
|
+
await assetPreservationCacheV2.put(
|
|
583
|
+
getCacheKey(),
|
|
584
|
+
preservedResponse
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
} catch (err) {
|
|
588
|
+
logError(err as Error);
|
|
589
|
+
}
|
|
590
|
+
})()
|
|
591
|
+
);
|
|
561
592
|
}
|
|
562
593
|
|
|
563
594
|
if (
|
|
@@ -585,15 +616,66 @@ export async function generateHandler<
|
|
|
585
616
|
|
|
586
617
|
async function notFound(): Promise<Response> {
|
|
587
618
|
if (caches) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
619
|
+
try {
|
|
620
|
+
const assetPreservationCacheV2 = await caches.open(
|
|
621
|
+
ASSET_PRESERVATION_CACHE_V2
|
|
622
|
+
);
|
|
623
|
+
let preservedResponse = await assetPreservationCacheV2.match(
|
|
624
|
+
getCacheKey()
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
// Continue serving from V1 preservation cache for some time to
|
|
628
|
+
// prevent 404s during the migration to V2
|
|
629
|
+
const cutoffDate = new Date("2024-05-10");
|
|
630
|
+
if (!preservedResponse && Date.now() < cutoffDate.getTime()) {
|
|
631
|
+
const assetPreservationCacheV1 = await caches.open(
|
|
632
|
+
ASSET_PRESERVATION_CACHE_V1
|
|
633
|
+
);
|
|
634
|
+
preservedResponse = await assetPreservationCacheV1.match(request.url);
|
|
635
|
+
if (preservedResponse) {
|
|
636
|
+
// V1 cache contains full response bodies so we return it directly
|
|
637
|
+
if (setMetrics) {
|
|
638
|
+
setMetrics({ preservationCacheResult: "checked-hit" });
|
|
639
|
+
}
|
|
640
|
+
return preservedResponse;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// V2 cache only contains the asset key, rather than the asset body:
|
|
645
|
+
if (preservedResponse) {
|
|
646
|
+
if (setMetrics) {
|
|
647
|
+
setMetrics({ preservationCacheResult: "checked-hit" });
|
|
648
|
+
}
|
|
649
|
+
// Always read the asset key to prevent hanging responses
|
|
650
|
+
const assetKey = await preservedResponse.text();
|
|
651
|
+
if (isNullBodyStatus(preservedResponse.status)) {
|
|
652
|
+
// We know the asset hasn't changed, so use the cached headers.
|
|
653
|
+
return new Response(null, preservedResponse);
|
|
654
|
+
}
|
|
655
|
+
if (assetKey) {
|
|
656
|
+
const asset = await fetchAsset(assetKey);
|
|
657
|
+
if (asset) {
|
|
658
|
+
// We know the asset hasn't changed, so use the cached headers.
|
|
659
|
+
return new Response(asset.body, preservedResponse);
|
|
660
|
+
} else {
|
|
661
|
+
logError(
|
|
662
|
+
new Error(
|
|
663
|
+
`preservation cache contained assetKey that does not exist in storage: ${assetKey}`
|
|
664
|
+
)
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
} else {
|
|
668
|
+
logError(new Error(`cached response had no assetKey: ${assetKey}`));
|
|
669
|
+
}
|
|
670
|
+
} else {
|
|
671
|
+
if (setMetrics) {
|
|
672
|
+
setMetrics({ preservationCacheResult: "checked-miss" });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
} catch (err) {
|
|
676
|
+
// Don't throw an error because preservation cache is best effort.
|
|
677
|
+
// But log it because we should be able to fetch the asset here.
|
|
678
|
+
logError(err as Error);
|
|
597
679
|
}
|
|
598
680
|
} else {
|
|
599
681
|
if (setMetrics) setMetrics({ preservationCacheResult: "disabled" });
|
|
@@ -662,6 +744,26 @@ function isPreview(url: URL): boolean {
|
|
|
662
744
|
return false;
|
|
663
745
|
}
|
|
664
746
|
|
|
747
|
+
/** Checks if a response is older than CACHE_PRESERVATION_WRITE_FREQUENCY
|
|
748
|
+
* and should be written to cache again to reset it's expiration.
|
|
749
|
+
*/
|
|
750
|
+
export function isPreservationCacheResponseExpiring(
|
|
751
|
+
response: Response
|
|
752
|
+
): boolean {
|
|
753
|
+
const ageHeader = response.headers.get("age");
|
|
754
|
+
if (!ageHeader) return false;
|
|
755
|
+
try {
|
|
756
|
+
const age = parseInt(ageHeader);
|
|
757
|
+
// Add up to 12 hours of jitter to help prevent a
|
|
758
|
+
// thundering heard when a lot of assets expire at once.
|
|
759
|
+
const jitter = Math.floor(Math.random() * 43_200);
|
|
760
|
+
if (age > CACHE_PRESERVATION_WRITE_FREQUENCY + jitter) return true;
|
|
761
|
+
} catch {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
|
|
665
767
|
/**
|
|
666
768
|
* Whether or not the passed in string looks like an HTML
|
|
667
769
|
* Content-Type header
|