feat(02-12.1): vary the record echo on the headers that produced it

A shared cache is one reverse proxy away, and without Vary the first one
to appear would key a delegated record response on URL alone and hand it
to the next request under a different context. Appends rather than
replaces, so a route that already varies keeps what it had.
This commit is contained in:
Marc-André Bombeck committed 2026-08-06 20:51:29 +02:00
1 parent dc343f434c
commit bcf2322f65
2 files changed
+85

No files matched your search

@@ -330,6 +330,64 @@ describe("the contract module is safe in the browser bundle", () => {
});
});
describe("attachRecordContextEcho", () => {
async function attach(
context: { epoch: number; scope: string | null } | undefined,
initial?: Record<string, string>,
): Promise<Headers> {
const { attachRecordContextEcho } =
await import("@/lib/sharing/record-session-fence");
const headers = new Headers(initial);
attachRecordContextEcho(headers, context);
return headers;
}
it("declares that the answer varies on the two fence headers", async () => {
// Without this a shared cache — one reverse proxy away — would key a
// delegated record response on URL alone and serve it to the next request
// for the same URL under a different context. One person's record handed
// to another by an infrastructure change nobody thought was a code change.
const headers = await attach({ epoch: 4, scope: "owner-1" });
const vary = (headers.get("Vary") ?? "")
.split(",")
.map((part) => part.trim().toLowerCase());
expect(vary).toContain(RECORD_EPOCH_HEADER);
expect(vary).toContain(RECORD_SCOPE_HEADER);
});
it("keeps a Vary the route had already set", async () => {
const headers = await attach(
{ epoch: 1, scope: null },
{ Vary: "Accept-Encoding" },
);
const vary = (headers.get("Vary") ?? "")
.split(",")
.map((part) => part.trim().toLowerCase());
expect(vary).toContain("accept-encoding");
expect(vary).toContain(RECORD_EPOCH_HEADER);
});
it("does not repeat a header the route already varied on", async () => {
const headers = await attach(
{ epoch: 1, scope: null },
{ Vary: `Accept-Encoding, ${RECORD_EPOCH_HEADER.toUpperCase()}` },
);
const vary = (headers.get("Vary") ?? "")
.split(",")
.map((part) => part.trim().toLowerCase());
expect(vary.filter((v) => v === RECORD_EPOCH_HEADER)).toHaveLength(1);
});
it("adds nothing at all when no record scope was resolved", async () => {
// The response carries no echo, so there is nothing for a cache to vary
// on — and claiming otherwise would fragment the cache key of every public
// route for no reason.
const headers = await attach(undefined);
expect(headers.get("Vary")).toBeNull();
expect(headers.get(RECORD_EPOCH_HEADER)).toBeNull();
});
});
describe("assertRecordSessionFence", () => {
beforeEach(() => {
headerJar.clear();
+27
View File
@@ -103,6 +103,33 @@ export function attachRecordContextEcho(
if (!context) return;
headers.set(RECORD_EPOCH_HEADER, String(context.epoch));
headers.set(RECORD_SCOPE_HEADER, recordScopeHeaderValue(context.scope));
// And declare that the answer DEPENDS on those two request headers.
//
// Nothing between the browser and this app caches today — the responses that
// reach here are private and same-origin. But a shared cache is one reverse
// proxy away, and without `Vary` the first one to appear would key a
// delegated record response on URL alone and hand it to the next request for
// the same URL under a different context. That is one person's health record
// served to another, produced by an infrastructure change nobody thought was
// a code change.
//
// Appended rather than assigned: a route that already varies on something
// (`Accept-Encoding`, a locale header) must keep it. Case-insensitive
// de-duplication, because a value repeated in two spellings is the sort of
// header that makes a proxy give up and cache nothing.
const existing = headers
.get("Vary")
?.split(",")
.map((part) => part.trim())
.filter((part) => part.length > 0);
const names = new Set((existing ?? []).map((part) => part.toLowerCase()));
const merged = [...(existing ?? [])];
for (const header of [RECORD_EPOCH_HEADER, RECORD_SCOPE_HEADER]) {
if (names.has(header.toLowerCase())) continue;
names.add(header.toLowerCase());
merged.push(header);
}
headers.set("Vary", merged.join(", "));
}
/**