Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions apps/server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,9 +479,17 @@ const openStaticFile = Effect.fn("openStaticFile")(function* (filePath: string)
// 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 file = yield* fileSystem
.open(filePath, { flag: "r" })
.pipe(Effect.orElseSucceed(() => null));
if (file !== null) {
const info = yield* file.stat;
return info.type === "File" ? { _tag: "Handle" as const, file, info } : null;
}
// The Electron asar layer implements stat and readFile but not open, so files inside
// app.asar have no file handle. Keep the path and read the bytes in one pass when a
// response body is due, so HEAD and 304 responses never read the file.
return { _tag: "Path" as const, filePath, info: pathInfo };
});

const streamStaticFile = (file: FileSystem.File, size: bigint) =>
Expand Down Expand Up @@ -614,6 +622,11 @@ const handleStaticAndDevRequest = Effect.fn("handleStaticAndDevRequest")(
const contentType = isHtml ? "text/html; charset=utf-8" : mimeType;
// 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.
if (opened._tag === "Path") {
const fileSystem = yield* FileSystem.FileSystem;
const bytes = yield* fileSystem.readFile(opened.filePath);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return HttpServerResponse.uint8Array(bytes, { headers, contentType });
}
return HttpServerResponse.stream(streamStaticFile(opened.file, fileInfo.size), {
headers,
contentType,
Expand Down
44 changes: 44 additions & 0 deletions apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1710,6 +1710,50 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
);

it.effect("serves static files when the platform has stat and readFile but no open", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const staticDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-static-no-open-" });
const html = "<html>handleless-static-ok</html>";
yield* fileSystem.writeFileString(path.join(staticDir, "index.html"), html);
// Mirrors the Electron asar layer: stat and readFile work, open has no result.
const handlelessFileSystem = FileSystem.FileSystem.of({
...fileSystem,
open: () => fileSystem.open(path.join(staticDir, "missing-handle-target"), { flag: "r" }),
});
yield* buildAppUnderTest({ config: { staticDir } }).pipe(
Effect.provideService(FileSystem.FileSystem, handlelessFileSystem),
);

const response = yield* HttpClient.get("/", { headers: { "accept-encoding": "identity" } });
assert.equal(response.status, 200);
assert.equal(response.headers["content-length"], String(html.length));
assert.equal(yield* response.text, html);
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
);

it.effect("does not read static bytes for an unchanged conditional request", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const staticDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-static-304-" });
yield* fileSystem.writeFileString(path.join(staticDir, "app.js"), "export const build = 1;");
// A handle-less platform: open fails, and a 304 must not read the file either.
const handlelessFileSystem = FileSystem.FileSystem.of({
...fileSystem,
open: () => fileSystem.open(path.join(staticDir, "missing-handle-target"), { flag: "r" }),
readFile: () => fileSystem.readFile(path.join(staticDir, "missing-read-target")),
});
yield* buildAppUnderTest({ config: { staticDir } }).pipe(
Effect.provideService(FileSystem.FileSystem, handlelessFileSystem),
);

const response = yield* HttpClient.get("/app.js", { headers: { "if-none-match": "*" } });
assert.equal(response.status, 304);
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
);

it.effect("revalidates static files without sending unchanged bodies", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
Expand Down
Loading