Skip to content

Commit

Permalink
Skip parsing Early Hints for known empty results
Browse files Browse the repository at this point in the history
  • Loading branch information
GregBrimble committed Dec 16, 2024
1 parent e0e7703 commit e533b97
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/proud-rules-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/pages-shared": patch
---

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.
81 changes: 81 additions & 0 deletions packages/pages-shared/__tests__/asset-server/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,87 @@ describe("asset-server handler", () => {
);
});

test("early hints should cache empty link headers", async () => {
const deploymentId = "deployment-" + Math.random();
const metadata = createMetadataObject({ deploymentId }) as Metadata;

const findAssetEntryForPath = async (path: string) => {
if (path === "/index.html") {
return "index.html";
}

return null;
};

// Create cache storage to reuse between requests
const { caches } = createCacheStorage();

const getResponse = async () =>
getTestResponse({
request: new Request("https://example.com/"),
metadata,
findAssetEntryForPath,
caches,
fetchAsset: () =>
Promise.resolve(
Object.assign(
new Response(`
<!DOCTYPE html>
<html>
<body>
<h1>I'm a teapot</h1>
</body>
</html>`),
{ contentType: "text/html" }
)
),
});

const { response, spies } = await getResponse();
expect(response.status).toBe(200);
// waitUntil should be called twice: once for asset-preservation, once for early hints
expect(spies.waitUntil.length).toBe(2);

await Promise.all(spies.waitUntil);

const earlyHintsCache = await caches.open(`eh:${deploymentId}`);
const earlyHintsRes = await earlyHintsCache.match("https://example.com/");

if (!earlyHintsRes) {
throw new Error(
"Did not match early hints cache on https://example.com/"
);
}

expect(earlyHintsRes.headers.get("link")).toBeNull();

// Do it again, but this time ensure that we didn't write to cache again
const { response: response2, spies: spies2 } = await getResponse();

expect(response2.status).toBe(200);
// waitUntil should only be called for asset-preservation
expect(spies2.waitUntil.length).toBe(1);

await Promise.all(spies2.waitUntil);

const earlyHintsRes2 = await earlyHintsCache.match("https://example.com/");

if (!earlyHintsRes2) {
throw new Error(
"Did not match early hints cache on https://example.com/"
);
}

expect(earlyHintsRes2.headers.get("link")).toBeNull();
});

test.todo(
"early hints should temporarily cache failures to parse links",
async () => {
// I couldn't figure out a way to make HTMLRewriter error out
}
);

describe("should serve deleted assets from preservation cache", async () => {
beforeEach(() => {
vi.useFakeTimers();
Expand Down
27 changes: 18 additions & 9 deletions packages/pages-shared/asset-server/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ export async function generateHandler<
const href = element.getAttribute("href") || undefined;
const rel = element.getAttribute("rel") || undefined;
const as = element.getAttribute("as") || undefined;
console.log(href, rel, as);
if (href && !href.startsWith("data:") && rel) {
links.push({ href, rel, as });
}
Expand All @@ -436,22 +437,30 @@ export async function generateHandler<
});

const linkHeader = preEarlyHintsHeaders.get("Link");
const earlyHintsHeaders = new Headers({
"Cache-Control": "max-age=2592000", // 30 days
});
if (linkHeader) {
await earlyHintsCache.put(
earlyHintsCacheKey,
new Response(null, {
headers: {
Link: linkHeader,
"Cache-Control": "max-age=2592000", // 30 days
},
})
);
earlyHintsHeaders.append("Link", linkHeader);
}
await earlyHintsCache.put(
earlyHintsCacheKey,
new Response(null, { headers: earlyHintsHeaders })
);
} catch (err) {
// Nbd if we fail here in the deferred 'waitUntil' work. We're probably trying to parse a malformed page or something.
// Totally fine to skip over any errors.
// If we need to debug something, you can uncomment the following:
// logError(err)
// In any case, let's not bother checking again for another day.
await earlyHintsCache.put(
earlyHintsCacheKey,
new Response(null, {
headers: {
"Cache-Control": "max-age=86400", // 1 day
},
})
);
}
})()
);
Expand Down

0 comments on commit e533b97

Please sign in to comment.