From 1e9c3079573b818b7891f048a7f91c28ecab0fe5 Mon Sep 17 00:00:00 2001 From: Leo Date: Thu, 10 Sep 2026 14:12:58 -0400 Subject: [PATCH 1/3] perf(server): stream static files and cache hashed build assets Self-hosted web reloads sent whole files with no validators. Hashed Vite manifest entries now get immutable caching. Other files, SPA HTML, and hash-like custom names revalidate with weak ETags. Each response reads metadata and bytes from one request-scoped handle so an atomic replace cannot mix old headers with a new body. Adapted from T3 Code #9669. Path traversal checks stay in place. Grok 4.6 High in Grok Build via Orca. --- apps/server/src/http.ts | 147 +++++++++++++--- apps/server/src/server.test.ts | 304 +++++++++++++++++++++++++++++++++ apps/web/vite.config.ts | 1 + 3 files changed, 430 insertions(+), 22 deletions(-) diff --git a/apps/server/src/http.ts b/apps/server/src/http.ts index 6a78e32ac7a2..33e9be51afc4 100644 --- a/apps/server/src/http.ts +++ b/apps/server/src/http.ts @@ -12,6 +12,8 @@ import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; import { cast } from "effect/Function"; import { HttpBody, @@ -279,10 +281,66 @@ export const attachmentUploadRouteLayer = HttpRouter.add( }), ); -export const staticAndDevRouteLayer = HttpRouter.add( - "GET", - "*", - Effect.gen(function* () { +const decodeBuildManifest = Schema.decodeUnknownEffect( + Schema.fromJsonString( + Schema.Record( + Schema.String, + Schema.Struct({ + file: Schema.String, + css: Schema.optional(Schema.Array(Schema.String)), + assets: Schema.optional(Schema.Array(Schema.String)), + }), + ), + ), +); + +const loadImmutableBuildAssets = Effect.gen(function* () { + const config = yield* ServerConfig.ServerConfig; + const staticDir = + config.staticDir ?? (config.devUrl ? yield* ServerConfig.resolveStaticDir() : undefined); + if (!staticDir) return new Set(); + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + return yield* fileSystem.readFileString(path.join(staticDir, ".vite", "manifest.json")).pipe( + Effect.flatMap(decodeBuildManifest), + Effect.map( + (manifest) => + new Set( + Object.values(manifest).flatMap((entry) => [ + entry.file, + ...(entry.css ?? []), + ...(entry.assets ?? []), + ]), + ), + ), + Effect.orElseSucceed(() => new Set()), + ); +}); + +const openStaticFile = Effect.fn("openStaticFile")(function* (filePath: string) { + const fileSystem = yield* FileSystem.FileSystem; + // Reject directories and special files before opening. Response metadata comes from the handle. + const pathInfo = yield* fileSystem.stat(filePath).pipe(Effect.orElseSucceed(() => null)); + if (pathInfo?.type !== "File") return null; + const file = yield* fileSystem.open(filePath, { flag: "r" }); + const info = yield* file.stat; + return info.type === "File" ? { file, info } : null; +}); + +const streamStaticFile = (file: FileSystem.File, size: bigint) => + Stream.unfold( + 0n, + Effect.fnUntraced(function* (offset: bigint) { + if (offset >= size) return; + const remaining = size - offset; + const bytes = yield* file.readAlloc(remaining < 65_536n ? remaining : 65_536n); + if (Option.isNone(bytes)) return; + return [bytes.value, offset + BigInt(bytes.value.byteLength)] as const; + }), + ); + +const handleStaticAndDevRequest = Effect.fn("handleStaticAndDevRequest")( + function* (immutableBuildAssets: ReadonlySet) { const request = yield* HttpServerRequest.HttpServerRequest; const url = HttpServerRequest.toURL(request); @@ -309,7 +367,6 @@ export const staticAndDevRouteLayer = HttpRouter.add( }); } - const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const staticRoot = path.resolve(staticDir); const staticRequestPath = url.value.pathname === "/" ? "/index.html" : url.value.pathname; @@ -343,30 +400,76 @@ export const staticAndDevRouteLayer = HttpRouter.add( } } - const fileInfo = yield* fileSystem.stat(filePath).pipe(Effect.orElseSucceed(() => null)); - if (!fileInfo || fileInfo.type !== "File") { - const indexPath = path.resolve(staticRoot, "index.html"); - const indexData = yield* fileSystem - .readFile(indexPath) - .pipe(Effect.orElseSucceed(() => null)); - if (!indexData) { + let opened = yield* openStaticFile(filePath); + if (!opened) { + filePath = path.resolve(staticRoot, "index.html"); + opened = yield* openStaticFile(filePath); + if (!opened) { return HttpServerResponse.text("Not Found", { status: 404 }); } - return HttpServerResponse.uint8Array(indexData, { - status: 200, - contentType: "text/html; charset=utf-8", - }); + } + const fileInfo = opened.info; + + // A hash-like name is not enough: custom static files can use the same naming pattern. + const relativePath = path.relative(staticRoot, filePath).replaceAll("\\", "/"); + const immutable = + /^assets\/.+-[\w-]{8}\.[^/]+$/.test(relativePath) && immutableBuildAssets.has(relativePath); + const headers: Record = { + "Cache-Control": immutable ? "public, max-age=31536000, immutable" : "no-cache", + }; + const modifiedAt = Option.getOrUndefined(fileInfo.mtime); + const etag = modifiedAt + ? `W/"${fileInfo.size.toString(16)}-${modifiedAt.getTime().toString(16)}"` + : undefined; + if (etag !== undefined && modifiedAt !== undefined) { + headers.ETag = etag; + headers["Last-Modified"] = modifiedAt.toUTCString(); } - const contentType = Mime.getType(filePath) ?? "application/octet-stream"; - const data = yield* fileSystem.readFile(filePath).pipe(Effect.orElseSucceed(() => null)); - if (!data) { - return HttpServerResponse.text("Internal Server Error", { status: 500 }); + // If-None-Match takes precedence over dates and uses weak comparison for + // GET/HEAD, including when compression changes the transferred bytes. + const ifNoneMatch = request.headers["if-none-match"]; + const ifModifiedSince = request.headers["if-modified-since"]; + const unchanged = + ifNoneMatch !== undefined + ? ifNoneMatch.split(",").some((value) => { + const candidate = value.trim(); + return ( + candidate === "*" || + (etag !== undefined && candidate.replace(/^W\//i, "") === etag.slice(2)) + ); + }) + : ifModifiedSince !== undefined && + modifiedAt !== undefined && + Date.parse(modifiedAt.toUTCString()) <= Date.parse(ifModifiedSince); + if (unchanged) { + return HttpServerResponse.empty({ + status: 304, + headers: { ...headers, Vary: "Accept-Encoding" }, + }); } - return HttpServerResponse.uint8Array(data, { - status: 200, + const contentType = + path.extname(filePath) === ".html" + ? "text/html; charset=utf-8" + : (Mime.getType(filePath) ?? "application/octet-stream"); + // The request scope closes the handle for GET, HEAD, 304, errors, and cancellation. + // HEAD still passes through compression, which selects headers without reading the stream. + return HttpServerResponse.stream(streamStaticFile(opened.file, fileInfo.size), { + headers, contentType, + contentLength: Number(fileInfo.size), }); + }, + Effect.catchTags({ + PlatformError: () => + Effect.succeed(HttpServerResponse.text("Internal Server Error", { status: 500 })), }), ); + +// Read the installed build's manifest once. Unknown files use revalidation. +export const staticAndDevRouteLayer = Layer.unwrap( + loadImmutableBuildAssets.pipe( + Effect.map((assets) => HttpRouter.add("GET", "*", handleStaticAndDevRequest(assets))), + ), +); diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 75af41f85ea0..5eaa434bdc0e 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -1452,6 +1452,310 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); + it.effect("revalidates static files without sending unchanged bodies", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const staticDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-static-cache-" }); + const indexPath = path.join(staticDir, "index.html"); + yield* fileSystem.writeFileString(indexPath, "first build"); + yield* buildAppUnderTest({ config: { staticDir } }); + + const initial = yield* HttpClient.get("/"); + assert.equal(initial.status, 200); + assert.equal(initial.headers["cache-control"], "no-cache"); + assert.include(yield* initial.text, "first build"); + const etag = initial.headers.etag; + assert.isDefined(etag); + assert.isDefined(initial.headers["last-modified"]); + + for (const headers of [ + { "if-none-match": etag! }, + { "if-none-match": `"older", ${etag!.replace(/^W\//, "")}` }, + { "if-none-match": "*" }, + { "if-modified-since": initial.headers["last-modified"]! }, + ]) { + const response = yield* HttpClient.get("/", { headers }); + assert.equal(response.status, 304); + assert.equal(response.headers.etag, etag); + assert.equal(response.headers["cache-control"], "no-cache"); + assert.equal(yield* response.text, ""); + } + + const mismatched = yield* HttpClient.get("/", { + headers: { + "if-none-match": '"another-build"', + "if-modified-since": initial.headers["last-modified"]!, + }, + }); + assert.equal(mismatched.status, 200); + assert.include(yield* mismatched.text, "first build"); + + yield* fileSystem.writeFileString(indexPath, "the next build is available"); + const changed = yield* HttpClient.get("/", { headers: { "if-none-match": etag! } }); + assert.equal(changed.status, 200); + assert.notEqual(changed.headers.etag, etag); + assert.include(yield* changed.text, "next build"); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + + it.effect("caches hashed static assets without freezing mutable files or SPA fallbacks", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const staticDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-static-hashes-" }); + yield* fileSystem.makeDirectory(path.join(staticDir, "assets")); + yield* fileSystem.makeDirectory(path.join(staticDir, ".vite")); + yield* fileSystem.writeFileString( + path.join(staticDir, ".vite", "manifest.json"), + `{ + "index.html": { "file": "assets/index-AbCd0123.js", "isEntry": true }, + "large.js": { "file": "assets/large-aBcD9876.js" } + }`, + ); + yield* fileSystem.writeFileString(path.join(staticDir, "index.html"), "app"); + yield* fileSystem.writeFileString( + path.join(staticDir, "assets", "index-AbCd0123.js"), + "export const app = true;", + ); + yield* fileSystem.writeFileString(path.join(staticDir, "assets", "config.json"), "{}"); + const largeAsset = "export const value = 123;\n".repeat(8192); + yield* fileSystem.writeFileString( + path.join(staticDir, "assets", "large-aBcD9876.js"), + largeAsset, + ); + yield* buildAppUnderTest({ config: { staticDir } }); + + const asset = yield* HttpClient.get("/assets/index-AbCd0123.js"); + assert.equal(asset.status, 200); + assert.equal(asset.headers["cache-control"], "public, max-age=31536000, immutable"); + assert.equal(yield* asset.text, "export const app = true;"); + + const head = yield* HttpClient.head("/assets/index-AbCd0123.js", { + headers: { "accept-encoding": "identity" }, + }); + assert.equal(head.status, 200); + assert.equal(head.headers.etag, asset.headers.etag); + assert.equal(head.headers["content-length"], String("export const app = true;".length)); + assert.equal(yield* head.text, ""); + + const compressed = yield* HttpClient.get("/assets/large-aBcD9876.js", { + headers: { "accept-encoding": "gzip" }, + }); + assert.equal(compressed.headers["content-encoding"], "gzip"); + assert.equal(compressed.headers.vary, "Accept-Encoding"); + assert.equal(yield* compressed.text, largeAsset); + const compressedHead = yield* HttpClient.head("/assets/large-aBcD9876.js", { + headers: { "accept-encoding": "gzip" }, + }); + assert.equal(compressedHead.status, 200); + assert.equal(compressedHead.headers["content-encoding"], "gzip"); + assert.equal(compressedHead.headers.vary, "Accept-Encoding"); + assert.equal(compressedHead.headers.etag, compressed.headers.etag); + assert.equal(compressedHead.headers["content-length"], compressed.headers["content-length"]); + assert.equal(yield* compressedHead.text, ""); + const unchanged = yield* HttpClient.get("/assets/large-aBcD9876.js", { + headers: { "accept-encoding": "identity", "if-none-match": compressed.headers.etag! }, + }); + assert.equal(unchanged.status, 304); + assert.equal(unchanged.headers.vary, "Accept-Encoding"); + assert.equal(yield* unchanged.text, ""); + + for (const resource of [ + "/assets/config.json", + "/threads/example", + "/assets/old-ZyXw9876.js", + ]) { + const response = yield* HttpClient.get(resource); + assert.equal(response.status, 200); + assert.equal(response.headers["cache-control"], "no-cache"); + assert.equal( + yield* response.text, + resource.endsWith("config.json") ? "{}" : "app", + ); + } + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + + for (const manifest of [ + { label: "missing", contents: null }, + { label: "nonmatching", contents: '{"other.js":{"file":"assets/other-AbCd0123.js"}}' }, + { label: "malformed", contents: "{not-json" }, + ]) { + it.effect(`revalidates hash-like static filenames with a ${manifest.label} manifest`, () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const staticDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-static-mutable-", + }); + yield* fileSystem.makeDirectory(path.join(staticDir, "assets")); + if (manifest.contents !== null) { + yield* fileSystem.makeDirectory(path.join(staticDir, ".vite")); + yield* fileSystem.writeFileString( + path.join(staticDir, ".vite", "manifest.json"), + manifest.contents, + ); + } + const filePath = path.join(staticDir, "assets", "config-20260904.js"); + yield* fileSystem.writeFileString(filePath, "first config"); + yield* buildAppUnderTest({ config: { staticDir } }); + + const initial = yield* HttpClient.get("/assets/config-20260904.js"); + assert.equal(initial.headers["cache-control"], "no-cache"); + assert.equal(yield* initial.text, "first config"); + + yield* fileSystem.writeFileString(filePath, "replacement config"); + const changed = yield* HttpClient.get("/assets/config-20260904.js", { + headers: { "if-none-match": initial.headers.etag! }, + }); + assert.equal(changed.status, 200); + assert.equal(changed.headers["cache-control"], "no-cache"); + assert.notEqual(changed.headers.etag, initial.headers.etag); + assert.equal(yield* changed.text, "replacement config"); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + } + + it.effect("binds static metadata and bytes to one file across atomic replacement", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const staticDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-static-replace-" }); + const beforeOpenPath = path.join(staticDir, "before-open.txt"); + const afterOpenPath = path.join(staticDir, "after-open.txt"); + const original = "original bytes"; + const replacement = "replacement bytes with a different size"; + for (const filePath of [beforeOpenPath, afterOpenPath]) { + yield* fileSystem.writeFileString(filePath, original); + yield* fileSystem.writeFileString(`${filePath}.next`, replacement); + } + const replaced = new Set(); + const replaceOnce = Effect.fnUntraced(function* (filePath: string) { + if (replaced.has(filePath)) return; + replaced.add(filePath); + yield* fileSystem.rename(`${filePath}.next`, filePath); + }); + const replacingFileSystem = FileSystem.FileSystem.of({ + ...fileSystem, + stat: (filePath) => + fileSystem + .stat(filePath) + .pipe( + Effect.tap(() => (filePath === beforeOpenPath ? replaceOnce(filePath) : Effect.void)), + ), + open: (filePath, options) => + fileSystem + .open(filePath, options) + .pipe( + Effect.tap(() => (filePath === afterOpenPath ? replaceOnce(filePath) : Effect.void)), + ), + }); + yield* buildAppUnderTest({ config: { staticDir } }).pipe( + Effect.provideService(FileSystem.FileSystem, replacingFileSystem), + ); + + for (const [name, expected] of [ + ["before-open.txt", replacement], + ["after-open.txt", original], + ] as const) { + const response = yield* HttpClient.get(`/${name}`, { + headers: { "accept-encoding": "identity" }, + }); + assert.equal(response.status, 200); + assert.equal(response.headers["content-length"], String(expected.length)); + assert.isTrue(response.headers.etag?.startsWith(`W/"${expected.length.toString(16)}-`)); + assert.equal(yield* response.text, expected); + assert.isTrue(replaced.has(path.join(staticDir, name))); + assert.equal(yield* fileSystem.readFileString(path.join(staticDir, name)), replacement); + } + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + + it.effect("closes static file handles after GET, HEAD, 304, and request cancellation", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const staticDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-static-close-" }); + const filePath = path.join(staticDir, "index.html"); + const body = "

file content

".repeat(1024); + yield* fileSystem.writeFileString(filePath, body); + const closed = yield* Queue.unbounded(); + const blocked = yield* Deferred.make(); + const active = new Set(); + let blockAfterOpen = false; + let bodyReads = 0; + const trackedFileSystem = FileSystem.FileSystem.of({ + ...fileSystem, + open: (candidate, options) => + Effect.gen(function* () { + if (candidate !== filePath) return yield* fileSystem.open(candidate, options); + let opened: FileSystem.File | undefined; + // Registered first, so this signal runs after the real descriptor-close finalizer. + yield* Effect.addFinalizer(() => + Effect.gen(function* () { + if (opened === undefined) return; + active.delete(opened); + yield* Queue.offer(closed, opened); + }), + ); + const file = yield* fileSystem.open(candidate, options); + opened = file; + active.add(file); + if (blockAfterOpen) { + yield* Deferred.succeed(blocked, undefined); + return yield* Effect.never; + } + return new Proxy(file, { + get(target, key) { + if (key === "readAlloc") { + return (size: FileSystem.SizeInput) => { + bodyReads += 1; + return target.readAlloc(size); + }; + } + return Reflect.get(target, key, target); + }, + }); + }), + }); + yield* buildAppUnderTest({ config: { staticDir } }).pipe( + Effect.provideService(FileSystem.FileSystem, trackedFileSystem), + ); + + const get = yield* HttpClient.get("/"); + assert.equal(yield* get.text, body); + yield* Queue.take(closed); + assert.equal(active.size, 0); + assert.isAbove(bodyReads, 0); + const readsAfterGet = bodyReads; + + const head = yield* HttpClient.head("/", { headers: { "accept-encoding": "gzip" } }); + assert.equal(head.status, 200); + assert.equal(head.headers["content-encoding"], "gzip"); + assert.equal(yield* head.text, ""); + yield* Queue.take(closed); + assert.equal(active.size, 0); + assert.equal(bodyReads, readsAfterGet); + + const unchanged = yield* HttpClient.get("/", { + headers: { "if-none-match": get.headers.etag! }, + }); + assert.equal(unchanged.status, 304); + yield* Queue.take(closed); + assert.equal(active.size, 0); + assert.equal(bodyReads, readsAfterGet); + + blockAfterOpen = true; + const cancelled = yield* HttpClient.get("/").pipe(Effect.forkChild); + yield* Deferred.await(blocked); + assert.equal(active.size, 1); + yield* Fiber.interrupt(cancelled); + yield* Queue.take(closed); + assert.equal(active.size, 0); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + it.effect("redirects to dev URL when configured", () => Effect.gen(function* () { yield* buildAppUnderTest({ diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 9e748a0a92e6..c7fa0eeeb3fe 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -269,6 +269,7 @@ export default defineConfig(() => { build: { outDir: "dist", emptyOutDir: true, + manifest: true, sourcemap: buildSourcemap, }, test: { From 2c9d25ff22e95cb28a211cff8d06bc55a14b9e77 Mon Sep 17 00:00:00 2001 From: Leo Date: Sun, 13 Sep 2026 21:16:08 -0400 Subject: [PATCH 2/3] fix(server): validate mutable static content --- apps/server/src/http.ts | 28 +++++++++++++++++---- apps/server/src/server.test.ts | 46 ++++++++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/apps/server/src/http.ts b/apps/server/src/http.ts index 33e9be51afc4..204dd674372e 100644 --- a/apps/server/src/http.ts +++ b/apps/server/src/http.ts @@ -1,4 +1,5 @@ import Mime from "@effect/platform-node/Mime"; +import * as NodeCrypto from "node:crypto"; import { AuthOrchestrationOperateScope, AuthOrchestrationReadScope, @@ -339,6 +340,20 @@ const streamStaticFile = (file: FileSystem.File, size: bigint) => }), ); +const hashStaticFile = Effect.fn("hashStaticFile")(function* (file: FileSystem.File, size: bigint) { + const hash = NodeCrypto.createHash("sha256"); + let offset = 0n; + while (offset < size) { + const remaining = size - offset; + const bytes = yield* file.readAlloc(remaining < 65_536n ? remaining : 65_536n); + if (Option.isNone(bytes)) break; + hash.update(bytes.value); + offset += BigInt(bytes.value.byteLength); + } + yield* file.seek(0, "start"); + return `"sha256-${hash.digest("hex")}"`; +}); + const handleStaticAndDevRequest = Effect.fn("handleStaticAndDevRequest")( function* (immutableBuildAssets: ReadonlySet) { const request = yield* HttpServerRequest.HttpServerRequest; @@ -418,9 +433,11 @@ const handleStaticAndDevRequest = Effect.fn("handleStaticAndDevRequest")( "Cache-Control": immutable ? "public, max-age=31536000, immutable" : "no-cache", }; const modifiedAt = Option.getOrUndefined(fileInfo.mtime); - const etag = modifiedAt - ? `W/"${fileInfo.size.toString(16)}-${modifiedAt.getTime().toString(16)}"` - : undefined; + const etag = immutable + ? modifiedAt + ? `W/"${fileInfo.size.toString(16)}-${modifiedAt.getTime().toString(16)}"` + : undefined + : yield* hashStaticFile(opened.file, fileInfo.size); if (etag !== undefined && modifiedAt !== undefined) { headers.ETag = etag; headers["Last-Modified"] = modifiedAt.toUTCString(); @@ -436,10 +453,11 @@ const handleStaticAndDevRequest = Effect.fn("handleStaticAndDevRequest")( const candidate = value.trim(); return ( candidate === "*" || - (etag !== undefined && candidate.replace(/^W\//i, "") === etag.slice(2)) + (etag !== undefined && candidate.replace(/^W\//i, "") === etag.replace(/^W\//i, "")) ); }) - : ifModifiedSince !== undefined && + : immutable && + ifModifiedSince !== undefined && modifiedAt !== undefined && Date.parse(modifiedAt.toUTCString()) <= Date.parse(ifModifiedSince); if (unchanged) { diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 0772fb6cf626..27adc5b5bd70 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -1529,9 +1529,8 @@ it.layer(NodeServices.layer)("server router seam", (it) => { for (const headers of [ { "if-none-match": etag! }, - { "if-none-match": `"older", ${etag!.replace(/^W\//, "")}` }, + { "if-none-match": `"older", W/${etag!}` }, { "if-none-match": "*" }, - { "if-modified-since": initial.headers["last-modified"]! }, ]) { const response = yield* HttpClient.get("/", { headers }); assert.equal(response.status, 304); @@ -1540,6 +1539,12 @@ it.layer(NodeServices.layer)("server router seam", (it) => { assert.equal(yield* response.text, ""); } + const dateOnly = yield* HttpClient.get("/", { + headers: { "if-modified-since": initial.headers["last-modified"]! }, + }); + assert.equal(dateOnly.status, 200); + assert.include(yield* dateOnly.text, "first build"); + const mismatched = yield* HttpClient.get("/", { headers: { "if-none-match": '"another-build"', @@ -1557,6 +1562,36 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); + it.effect("changes mutable validators when equal-size content keeps its modification time", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const staticDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-static-etag-" }); + const indexPath = path.join(staticDir, "index.html"); + const original = "build one"; + const replacement = "build two"; + assert.equal(original.length, replacement.length); + yield* fileSystem.writeFileString(indexPath, original); + const originalInfo = yield* fileSystem.stat(indexPath); + const originalMtime = Option.getOrThrow(originalInfo.mtime); + yield* buildAppUnderTest({ config: { staticDir } }); + + const initial = yield* HttpClient.get("/"); + const initialEtag = initial.headers.etag; + assert.equal(yield* initial.text, original); + assert.isDefined(initialEtag); + + yield* fileSystem.writeFileString(indexPath, replacement); + yield* fileSystem.utimes(indexPath, originalMtime, originalMtime); + const changed = yield* HttpClient.get("/", { + headers: { "if-none-match": initialEtag! }, + }); + assert.equal(changed.status, 200); + assert.notEqual(changed.headers.etag, initialEtag); + assert.equal(yield* changed.text, replacement); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + it.effect("caches hashed static assets without freezing mutable files or SPA fallbacks", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; @@ -1722,7 +1757,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }); assert.equal(response.status, 200); assert.equal(response.headers["content-length"], String(expected.length)); - assert.isTrue(response.headers.etag?.startsWith(`W/"${expected.length.toString(16)}-`)); + assert.isTrue(response.headers.etag?.startsWith('"sha256-')); assert.equal(yield* response.text, expected); assert.isTrue(replaced.has(path.join(staticDir, name))); assert.equal(yield* fileSystem.readFileString(path.join(staticDir, name)), replacement); @@ -1794,7 +1829,8 @@ it.layer(NodeServices.layer)("server router seam", (it) => { assert.equal(yield* head.text, ""); yield* Queue.take(closed); assert.equal(active.size, 0); - assert.equal(bodyReads, readsAfterGet); + assert.isAbove(bodyReads, readsAfterGet); + const readsAfterHead = bodyReads; const unchanged = yield* HttpClient.get("/", { headers: { "if-none-match": get.headers.etag! }, @@ -1802,7 +1838,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { assert.equal(unchanged.status, 304); yield* Queue.take(closed); assert.equal(active.size, 0); - assert.equal(bodyReads, readsAfterGet); + assert.isAbove(bodyReads, readsAfterHead); blockAfterOpen = true; const cancelled = yield* HttpClient.get("/").pipe(Effect.forkChild); From 2c744d5f8c2242a41d3d214c18f06266394b5d97 Mon Sep 17 00:00:00 2001 From: Leo Date: Sun, 13 Sep 2026 21:34:25 -0400 Subject: [PATCH 3/3] fix(server): avoid reading static files for validation --- apps/server/src/http.ts | 27 +++++++++++++-------------- apps/server/src/server.test.ts | 9 ++++----- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/apps/server/src/http.ts b/apps/server/src/http.ts index 204dd674372e..9409a0e65fe8 100644 --- a/apps/server/src/http.ts +++ b/apps/server/src/http.ts @@ -1,5 +1,6 @@ +// @effect-diagnostics nodeBuiltinImport:off import Mime from "@effect/platform-node/Mime"; -import * as NodeCrypto from "node:crypto"; +import * as NodeFS from "node:fs"; import { AuthOrchestrationOperateScope, AuthOrchestrationReadScope, @@ -340,19 +341,17 @@ const streamStaticFile = (file: FileSystem.File, size: bigint) => }), ); -const hashStaticFile = Effect.fn("hashStaticFile")(function* (file: FileSystem.File, size: bigint) { - const hash = NodeCrypto.createHash("sha256"); - let offset = 0n; - while (offset < size) { - const remaining = size - offset; - const bytes = yield* file.readAlloc(remaining < 65_536n ? remaining : 65_536n); - if (Option.isNone(bytes)) break; - hash.update(bytes.value); - offset += BigInt(bytes.value.byteLength); +function mutableFileEtag(file: FileSystem.File): string | undefined { + const descriptor = (file as FileSystem.File & { readonly fd?: unknown }).fd; + if (typeof descriptor !== "number") return undefined; + try { + const info = NodeFS.fstatSync(descriptor, { bigint: true }); + return `W/"${info.dev.toString(16)}-${info.ino.toString(16)}-${info.size.toString(16)}-${info.mtimeNs.toString(16)}-${info.ctimeNs.toString(16)}"`; + } catch { + // Without a validator that changes with content, serving 200 is safer than a stale 304. + return undefined; } - yield* file.seek(0, "start"); - return `"sha256-${hash.digest("hex")}"`; -}); +} const handleStaticAndDevRequest = Effect.fn("handleStaticAndDevRequest")( function* (immutableBuildAssets: ReadonlySet) { @@ -437,7 +436,7 @@ const handleStaticAndDevRequest = Effect.fn("handleStaticAndDevRequest")( ? modifiedAt ? `W/"${fileInfo.size.toString(16)}-${modifiedAt.getTime().toString(16)}"` : undefined - : yield* hashStaticFile(opened.file, fileInfo.size); + : mutableFileEtag(opened.file); if (etag !== undefined && modifiedAt !== undefined) { headers.ETag = etag; headers["Last-Modified"] = modifiedAt.toUTCString(); diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 27adc5b5bd70..b30f98a98e26 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -1529,7 +1529,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { for (const headers of [ { "if-none-match": etag! }, - { "if-none-match": `"older", W/${etag!}` }, + { "if-none-match": `"older", ${etag!.replace(/^W\//, "")}` }, { "if-none-match": "*" }, ]) { const response = yield* HttpClient.get("/", { headers }); @@ -1757,7 +1757,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }); assert.equal(response.status, 200); assert.equal(response.headers["content-length"], String(expected.length)); - assert.isTrue(response.headers.etag?.startsWith('"sha256-')); + assert.isDefined(response.headers.etag); assert.equal(yield* response.text, expected); assert.isTrue(replaced.has(path.join(staticDir, name))); assert.equal(yield* fileSystem.readFileString(path.join(staticDir, name)), replacement); @@ -1829,8 +1829,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { assert.equal(yield* head.text, ""); yield* Queue.take(closed); assert.equal(active.size, 0); - assert.isAbove(bodyReads, readsAfterGet); - const readsAfterHead = bodyReads; + assert.equal(bodyReads, readsAfterGet); const unchanged = yield* HttpClient.get("/", { headers: { "if-none-match": get.headers.etag! }, @@ -1838,7 +1837,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { assert.equal(unchanged.status, 304); yield* Queue.take(closed); assert.equal(active.size, 0); - assert.isAbove(bodyReads, readsAfterHead); + assert.equal(bodyReads, readsAfterGet); blockAfterOpen = true; const cancelled = yield* HttpClient.get("/").pipe(Effect.forkChild);