Build and Evaluate HTTP Cache Headers
Generate Cache-Control and ETag headers, then evaluate If-None-Match and If-Modified-Since to return 304 or 200 responses correctly.
Code
import { createHash } from "node:crypto";
function getHeader(headers, name) {
const key = Object.keys(headers).find(
(candidate) => candidate.toLowerCase() === name,
);
return key === undefined ? undefined : headers[key];
}
function requireNonNegativeInteger(value, label) {
if (!Number.isInteger(value) || value < 0) {
throw new RangeError(`${label} must be a non-negative integer`);
}
}
export function createEtag(body, options = {}) {
if (typeof body !== "string" && !Buffer.isBuffer(body)) {
throw new TypeError("body must be a string or Buffer");
}
const { weak = false } = options;
const digest = createHash("sha256")
.update(body)
.digest("base64url")
.slice(0, 27);
return `${weak ? "W/" : ""}"${digest}"`;
}
export function buildCacheControl(options = {}) {
const {
visibility = "public",
maxAge = 0,
sharedMaxAge,
staleWhileRevalidate,
immutable = false,
} = options;
if (visibility !== "public" && visibility !== "private") {
throw new RangeError("visibility must be public or private");
}
requireNonNegativeInteger(maxAge, "maxAge");
const directives = [visibility, `max-age=${maxAge}`];
if (sharedMaxAge !== undefined) {
requireNonNegativeInteger(sharedMaxAge, "sharedMaxAge");
directives.push(`s-maxage=${sharedMaxAge}`);
}
if (staleWhileRevalidate !== undefined) {
requireNonNegativeInteger(staleWhileRevalidate, "staleWhileRevalidate");
directives.push(`stale-while-revalidate=${staleWhileRevalidate}`);
}
if (immutable) {
if (maxAge === 0) {
throw new RangeError("immutable requires maxAge greater than zero");
}
directives.push("immutable");
}
return directives.join(", ");
}
export function cacheHeaders(options = {}) {
const {
body,
maxAge = 0,
visibility = "public",
lastModified,
weak = false,
staleWhileRevalidate,
} = options;
if (body === undefined) {
throw new TypeError("body is required");
}
const headers = {
"Cache-Control": buildCacheControl({
visibility,
maxAge,
staleWhileRevalidate,
}),
ETag: createEtag(body, { weak }),
};
if (lastModified !== undefined) {
const date = new Date(lastModified);
if (Number.isNaN(date.getTime())) {
throw new RangeError("lastModified must be a valid date");
}
headers["Last-Modified"] = date.toUTCString();
}
return headers;
}
function opaqueTag(tag) {
return tag.trim().replace(/^W\//i, "");
}
export function evaluateConditionalRequest(
method,
requestHeaders = {},
responseHeaders = {},
) {
const normalizedMethod =
typeof method === "string" ? method.toUpperCase() : "";
if (normalizedMethod !== "GET" && normalizedMethod !== "HEAD") {
return { status: 200, headers: { ...responseHeaders } };
}
const etag = getHeader(responseHeaders, "etag");
const lastModified = getHeader(responseHeaders, "last-modified");
const cacheControl = getHeader(responseHeaders, "cache-control");
const ifNoneMatch = getHeader(requestHeaders, "if-none-match");
let notModified = false;
if (ifNoneMatch !== undefined && etag !== undefined) {
const candidates = ifNoneMatch.split(",").map((tag) => tag.trim());
notModified =
candidates.includes("*") ||
candidates.some((tag) => opaqueTag(tag) === opaqueTag(etag));
} else {
const ifModifiedSince = getHeader(requestHeaders, "if-modified-since");
if (ifModifiedSince !== undefined && lastModified !== undefined) {
const since = Date.parse(ifModifiedSince);
const modified = Date.parse(lastModified);
if (!Number.isNaN(since) && !Number.isNaN(modified)) {
notModified =
Math.floor(modified / 1000) <= Math.floor(since / 1000);
}
}
}
if (!notModified) {
return { status: 200, headers: { ...responseHeaders } };
}
const headers = {};
if (etag !== undefined) headers.ETag = etag;
if (cacheControl !== undefined) headers["Cache-Control"] = cacheControl;
if (lastModified !== undefined) headers["Last-Modified"] = lastModified;
return { status: 304, headers };
}
const body = JSON.stringify({ items: [1, 2, 3] });
const headers = cacheHeaders({
body,
maxAge: 60,
staleWhileRevalidate: 30,
lastModified: "2026-10-21T10:00:00Z",
});
console.log(headers);
console.log(evaluateConditionalRequest("GET", {}, headers).status);
console.log(
evaluateConditionalRequest("GET", { "if-none-match": headers.ETag }, headers)
.status,
);
console.log(
evaluateConditionalRequest(
"GET",
{ "if-modified-since": headers["Last-Modified"] },
headers,
).status,
);
console.log(
evaluateConditionalRequest("POST", { "if-none-match": headers.ETag }, headers)
.status,
);Report a problem with this exampleOpens a pre-filled issue on GitHub.