SXO ETag and Caching: How Hashed vs Non-Hashed Assets Are Handled
SXO assigns immutable year-long caches to hashed assets and five-minute caches to non-hashed files, generating weak ETags from file metadata for conditional requests while deliberately omitting ETags on pre-compressed variants to prevent cache mismatches.
The gc-victor/sxo repository implements a deterministic static file serving strategy that differentiates cache behavior based on filename hashing. Understanding how SXO handles ETag and caching for hashed vs non-hashed assets reveals how the framework optimizes long-term asset storage while keeping dynamic content fresh through precise header control.
Cache-Control Policies for Asset Types
Hashed Assets: Immutable Long-Term Caching
Files containing content hashes in their filenames receive the CACHE_IMMUTABLE directive defined in src/js/server/shared/cache.js. This header value is public, max-age=31536000, immutable, instructing browsers to cache the resource for one year without revalidation attempts. The isHashedAsset() function in src/js/server/shared/path.js detects these files using a regex that matches hexadecimal strings of eight or more characters or esbuild-style base36 hash segments.
Non-Hashed Assets: Short-Lived Caching
Standard files without hash segments in their names receive the CACHE_SHORT constant (public, max-age=300), establishing a five-minute cache lifetime. This ensures that unversioned resources remain fresh while still benefiting from client-side caching between updates.
ETag Generation and Validation Strategy
Weak ETag Construction from File Stats
The weakEtagFromStat() utility in src/js/server/shared/cache.js generates validators by combining the file's size and modification time into a hexadecimal string formatted as W/"sizeHex-mtimeHex". This helper normalizes the stat object interface to support both Node.js (mtimeMs property) and Deno (mtime Date object) environments.
// src/js/server/shared/cache.js
export function weakEtagFromStat(stat) {
let mtimeMs = 0;
if (typeof stat.mtimeMs === "number") {
mtimeMs = stat.mtimeMs;
} else if (stat.mtime instanceof Date) {
mtimeMs = stat.mtime.getTime();
}
return weakEtag(stat.size, mtimeMs);
}
ETag Omission for Compressed Variants
When serving pre-compressed .br or .gz file variants, SXO explicitly sets the ETag header to null. Since the response depends on the client's Accept-Encoding negotiation, including an ETag could cause cache collisions where a browser matches an uncompressed representation against a compressed request, or vice versa.
Implementation Flow in the Static Handler
The request handling logic resides in src/js/server/shared/static-handler.js and executes the following steps:
-
Asset Classification: Extract the basename using
getBasename(absPath)and testisHashedAsset(basename)to select betweenCACHE_IMMUTABLEandCACHE_SHORT. -
File Statistics: Obtain metadata via
fileReader.stat(sendPath)to access size and modification time. -
Conditional Validation: Compare the client's
If-None-Matchheader against the computed weak ETag. When matched, return 304 Not Modified with the currentCache-ControlandETagheaders. -
Header Assembly: Construct the response with
Content-Type,Cache-Control, andETag(when applicable). AddVary: Accept-Encodingfor negotiated responses.
// src/js/server/shared/static-handler.js
const basename = getBasename(absPath);
const effectiveCacheControl = cacheControl ?? (isHashedAsset(basename) ? CACHE_IMMUTABLE : CACHE_SHORT);
const stat = await fileReader.stat(sendPath);
const etag = contentEncoding ? null : weakEtagFromStat(stat);
if (etag) {
const ifNoneMatch = request.headers.get("If-None-Match");
if (ifNoneMatch === etag) {
return new Response(null, {
status: HTTP_STATUS_NOT_MODIFIED,
headers: {
"Cache-Control": effectiveCacheControl,
ETag: etag,
},
});
}
}
const headers = new Headers({
"Content-Type": mimeType,
"Cache-Control": effectiveCacheControl,
});
if (etag) headers.set("ETag", etag);
if (!skipCompression) headers.set("Vary", "Accept-Encoding");
Summary
- Hashed assets receive one-year immutable caching via
CACHE_IMMUTABLEinsrc/js/server/shared/cache.js, triggered byisHashedAsset()detection insrc/js/server/shared/path.js. - Non-hashed assets use short-lived five-minute caching through the
CACHE_SHORTconstant. - Weak ETags are generated from file size and mtime using
weakEtagFromStat()to support conditional 304 responses. - Compressed variants intentionally omit ETags to prevent encoding-specific cache mismatches.
- Conditional requests are validated against the weak ETag in
src/js/server/shared/static-handler.jsbefore serving file contents.
Frequently Asked Questions
How does SXO detect whether a file contains a content hash?
SXO uses the isHashedAsset() function in src/js/server/shared/path.js, which applies the regex /(?:^|\.|-)(?:[a-f0-9]{8,}|[A-Z0-9]{8})(?:\.|$)/ to detect hexadecimal strings of eight or more characters or base36 hash segments within the filename.
Why does SXO serve compressed files without ETag headers?
The static handler omits ETags for pre-compressed .br and .gz variants because the same logical resource serves different byte representations based on Accept-Encoding negotiation. Including an ETag could cause browsers to incorrectly match a compressed representation with an uncompressed request during cache validation.
What happens when a client sends a matching If-None-Match header?
When the client's If-None-Match value matches the weak ETag generated by weakEtagFromStat(), SXO returns an HTTP 304 Not Modified response containing the Cache-Control and ETag headers but no response body, eliminating redundant data transfer for unchanged resources.
What is the format of SXO's weak ETags?
The weakEtag() helper produces tags in the format W/"sizeHex-mtimeHex", where the file size and modification timestamp are converted to hexadecimal strings and concatenated to create a unique validator for the resource state.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →