From 3e275311f1726e5ed5a627df4ddab47d31148987 Mon Sep 17 00:00:00 2001 From: tdurieux Date: Sun, 6 Sep 2026 09:17:47 +0200 Subject: [PATCH] fix: derive file ETags from current source content --- src/server/routes/file-etag.ts | 6 +++--- src/server/routes/file.ts | 2 +- test/production-regressions.test.js | 9 +++++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/server/routes/file-etag.ts b/src/server/routes/file-etag.ts index a65fe0b..10f01a7 100644 --- a/src/server/routes/file-etag.ts +++ b/src/server/routes/file-etag.ts @@ -1,17 +1,17 @@ import { createHash } from "crypto"; -// Build an ETag that fingerprints the upstream content (?v=), the file +// Build an ETag that fingerprints the current upstream content (commit and blob SHA), the file // path, and the anonymization config the user has saved. Without the config // part the browser kept serving bytes anonymized under an older term list // (#439). The path is folded in so two different files inside the same repo // can never collide. export function fileETag( - versionParam: string | undefined, + sourceFingerprint: string | undefined, filePath: string, options: unknown ): string { const h = createHash("sha1"); - h.update(versionParam || ""); + h.update(sourceFingerprint || ""); h.update("|"); h.update(filePath || ""); h.update("|"); diff --git a/src/server/routes/file.ts b/src/server/routes/file.ts index f649820..5802d26 100644 --- a/src/server/routes/file.ts +++ b/src/server/routes/file.ts @@ -132,7 +132,7 @@ router.get( ); } const etag = fileETag( - req.query.v as string | undefined, + `${repo.model.source.commit || ""}:${await f.sha() || ""}`, anonymizedPath, repo.model.options ); diff --git a/test/production-regressions.test.js b/test/production-regressions.test.js index 5854c7f..b9820ea 100644 --- a/test/production-regressions.test.js +++ b/test/production-regressions.test.js @@ -142,10 +142,15 @@ describe("production regressions", function () { await handler(req, response()); expect(failure.message).to.equal("file_not_supported"); }); - it("sandboxes renamed HTML documents", async function () { - const { handler, req } = fileRoute("page.html"); + it("sandboxes renamed HTML and invalidates stale client versions", async function () { + const { handler, req, repo } = fileRoute("page.html"); const first = response(); await handler(req, first); expect(first.headers["Content-Security-Policy"]).to.include("sandbox"); + repo.model.source.commit = "commit-2"; + req.headers["if-none-match"] = first.headers.ETag; + const second = response(); await handler(req, second); + expect(second.headers.ETag).not.to.equal(first.headers.ETag); + expect(second.statusCode).not.to.equal(304); }); it("omits an upstream length when later text is rewritten", async function () { const File = require("../src/core/AnonymizedFile").default;