Skip to content

Commit bcd2020

Browse files
GregBrimblepenalosa
authored andcommitted
Skip parsing Early Hints for known empty results (#7561)
* Remove now-unused asset preservation cache v1 from Pages * Refactor ETag handling * Check if-none-match before fulfilling preservation cache * Skip parsing Early Hints for known empty results
1 parent 9db94a9 commit bcd2020

File tree

5 files changed

+113
-9
lines changed

5 files changed

+113
-9
lines changed

.changeset/fuzzy-nails-invent.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/pages-shared": minor
3+
---
4+
5+
feat: Return a 304 Not Modified response when matching an asset preservation cache request if appropriate

.changeset/green-socks-trade.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/pages-shared": patch
3+
---
4+
5+
chore: Remove now-unused asset preservation cache (v1)

.changeset/proud-rules-try.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/pages-shared": patch
3+
---
4+
5+
fix: Store an empty result when Early Hints parsing returns nothing or errors. Previously, we weren't storing anything which resulted in Early Hints being parsed on every request.

packages/pages-shared/__tests__/asset-server/handler.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,87 @@ describe("asset-server handler", () => {
510510
);
511511
});
512512

513+
test("early hints should cache empty link headers", async () => {
514+
const deploymentId = "deployment-" + Math.random();
515+
const metadata = createMetadataObject({ deploymentId }) as Metadata;
516+
517+
const findAssetEntryForPath = async (path: string) => {
518+
if (path === "/index.html") {
519+
return "index.html";
520+
}
521+
522+
return null;
523+
};
524+
525+
// Create cache storage to reuse between requests
526+
const { caches } = createCacheStorage();
527+
528+
const getResponse = async () =>
529+
getTestResponse({
530+
request: new Request("https://example.com/"),
531+
metadata,
532+
findAssetEntryForPath,
533+
caches,
534+
fetchAsset: () =>
535+
Promise.resolve(
536+
Object.assign(
537+
new Response(`
538+
<!DOCTYPE html>
539+
<html>
540+
<body>
541+
<h1>I'm a teapot</h1>
542+
</body>
543+
</html>`),
544+
{ contentType: "text/html" }
545+
)
546+
),
547+
});
548+
549+
const { response, spies } = await getResponse();
550+
expect(response.status).toBe(200);
551+
// waitUntil should be called twice: once for asset-preservation, once for early hints
552+
expect(spies.waitUntil.length).toBe(2);
553+
554+
await Promise.all(spies.waitUntil);
555+
556+
const earlyHintsCache = await caches.open(`eh:${deploymentId}`);
557+
const earlyHintsRes = await earlyHintsCache.match("https://example.com/");
558+
559+
if (!earlyHintsRes) {
560+
throw new Error(
561+
"Did not match early hints cache on https://example.com/"
562+
);
563+
}
564+
565+
expect(earlyHintsRes.headers.get("link")).toBeNull();
566+
567+
// Do it again, but this time ensure that we didn't write to cache again
568+
const { response: response2, spies: spies2 } = await getResponse();
569+
570+
expect(response2.status).toBe(200);
571+
// waitUntil should only be called for asset-preservation
572+
expect(spies2.waitUntil.length).toBe(1);
573+
574+
await Promise.all(spies2.waitUntil);
575+
576+
const earlyHintsRes2 = await earlyHintsCache.match("https://example.com/");
577+
578+
if (!earlyHintsRes2) {
579+
throw new Error(
580+
"Did not match early hints cache on https://example.com/"
581+
);
582+
}
583+
584+
expect(earlyHintsRes2.headers.get("link")).toBeNull();
585+
});
586+
587+
test.todo(
588+
"early hints should temporarily cache failures to parse links",
589+
async () => {
590+
// I couldn't figure out a way to make HTMLRewriter error out
591+
}
592+
);
593+
513594
describe("should serve deleted assets from preservation cache", async () => {
514595
beforeEach(() => {
515596
vi.useFakeTimers();

packages/pages-shared/asset-server/handler.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -436,22 +436,30 @@ export async function generateHandler<
436436
});
437437

438438
const linkHeader = preEarlyHintsHeaders.get("Link");
439+
const earlyHintsHeaders = new Headers({
440+
"Cache-Control": "max-age=2592000", // 30 days
441+
});
439442
if (linkHeader) {
440-
await earlyHintsCache.put(
441-
earlyHintsCacheKey,
442-
new Response(null, {
443-
headers: {
444-
Link: linkHeader,
445-
"Cache-Control": "max-age=2592000", // 30 days
446-
},
447-
})
448-
);
443+
earlyHintsHeaders.append("Link", linkHeader);
449444
}
445+
await earlyHintsCache.put(
446+
earlyHintsCacheKey,
447+
new Response(null, { headers: earlyHintsHeaders })
448+
);
450449
} catch (err) {
451450
// Nbd if we fail here in the deferred 'waitUntil' work. We're probably trying to parse a malformed page or something.
452451
// Totally fine to skip over any errors.
453452
// If we need to debug something, you can uncomment the following:
454453
// logError(err)
454+
// In any case, let's not bother checking again for another day.
455+
await earlyHintsCache.put(
456+
earlyHintsCacheKey,
457+
new Response(null, {
458+
headers: {
459+
"Cache-Control": "max-age=86400", // 1 day
460+
},
461+
})
462+
);
455463
}
456464
})()
457465
);

0 commit comments

Comments
 (0)