From 2f11a7ab0513891dc1eb11f7abfadf0f7a6972e1 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Thu, 11 Apr 2024 10:29:48 +0200 Subject: Enable ETag-based browser caching --- src/app.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/app.ts b/src/app.ts index a656ca5..d3a676f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -38,6 +38,7 @@ type File = { type: string; size: number; mtime: Date; + etag: string; }; const metrics = { @@ -50,6 +51,7 @@ const metrics = { "hostname", "method", "content_encoding", + "cache_basis", ] as const, }), requestDuration: new prom.Histogram({ @@ -61,18 +63,25 @@ const metrics = { let files = new Map(); -function registerFile( +async function hashFile(file: BunFile): Promise { + return new Bun.CryptoHasher("sha256") + .update(await file.arrayBuffer()) + .digest("base64"); +} + +async function registerFile( path: string, pathname: string, filename: string, stat: Stats, -): void { +): Promise { pathname = "/" + (pathname === "." || pathname === "./" ? "" : pathname); if (files.get(pathname) !== undefined) { log.warn("File already registered:", pathname); } const handle = Bun.file(filename); + files.set(pathname, { filename, relPath: "/" + path, @@ -84,6 +93,7 @@ function registerFile( : undefined, size: stat.size, mtime: stat.mtime, + etag: `W/"${await hashFile(handle)}"`, }); } @@ -94,9 +104,9 @@ async function walkDirectory(root: string) { if (stat.isFile()) { if (relPath.includes("index.html")) { const dir = relPath.replace("index.html", ""); - registerFile(relPath, dir, absPath, stat); + await registerFile(relPath, dir, absPath, stat); } else { - registerFile(relPath, relPath, absPath, stat); + await registerFile(relPath, relPath, absPath, stat); } } } @@ -180,17 +190,19 @@ export const server = { let contentEncoding = "identity"; let suffix = ""; if (file && (await file.handle.exists())) { - if ( + let etagMatch = request.headers.get("if-none-match") === file.etag; + let mtimeMatch = parseIfModifiedSinceHeader( request.headers.get("if-modified-since"), - ) >= file?.mtime.getTime() - ) { + ) >= file?.mtime.getTime(); + if (etagMatch || mtimeMatch) { metrics.requests.inc({ method: request.method, hostname, content_encoding: contentEncoding, path: pathname, status_code: (status = 304), + cache_basis: etagMatch ? "etag" : "mtime", }); transaction.setHttpStatus(304); return new Response("", { status: status, headers: defaultHeaders }); @@ -225,6 +237,8 @@ export const server = { return serveFile(endFile, status, { "content-encoding": contentEncoding, "content-type": file.type, + // weak etags can be used for multiple equivalent representations + etag: file.etag, }); } else { if (files.has(pathname + "/")) { -- cgit 1.4.1