fix(router): don't cache file descriptors in Route.parse to prevent stale fd reuse (#27164)

## Summary
- `FileSystemRouter.Route.parse()` was caching file descriptors in the
global entry cache (`entry.cache.fd`). When `Bun.build()` later closed
these fds during `ParseTask`, the cache still referenced them.
Subsequent `Bun.build()` calls would find these stale fds, pass them to
`readFileWithAllocator`, and `seekTo(0)` would fail with EBADF (errno
9).
- The fix ensures `Route.parse` always closes the file it opens for
`getFdPath` instead of caching it in the shared entry. The fd was only
used to resolve the absolute path via `getFdPath`, so caching was
unnecessary and harmful.

Closes #18242

## Test plan
- [x] Added regression test `test/regression/issue/18242.test.ts` that
creates a `FileSystemRouter` and runs `Bun.build()` three times
sequentially
- [x] Test passes with `bun bd test test/regression/issue/18242.test.ts`
- [x] Test fails with `USE_SYSTEM_BUN=1 bun test
test/regression/issue/18242.test.ts` (system bun v1.3.9)
- [x] Verified 5 sequential builds work correctly after the fix

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
robobun
2026-02-19 20:15:11 -08:00
committed by GitHub
parent 044bb00382
commit ecd4e680eb
2 changed files with 65 additions and 4 deletions

View File

@@ -731,23 +731,22 @@ pub const Route = struct {
if (abs_path_str.len == 0) {
var file: std.fs.File = undefined;
var needs_close = false;
var needs_close = true;
defer if (needs_close) file.close();
if (entry.cache.fd.unwrapValid()) |valid| {
file = valid.stdFile();
needs_close = false;
} else {
var parts = [_]string{ entry.dir, entry.base() };
abs_path_str = FileSystem.instance.absBuf(&parts, &route_file_buf);
route_file_buf[abs_path_str.len] = 0;
const buf = route_file_buf[0..abs_path_str.len :0];
file = std.fs.openFileAbsoluteZ(buf, .{ .mode = .read_only }) catch |err| {
needs_close = false;
log.addErrorFmt(null, Logger.Loc.Empty, allocator, "{s} opening route: {s}", .{ @errorName(err), abs_path_str }) catch unreachable;
return null;
};
FileSystem.setMaxFd(file.handle);
needs_close = FileSystem.instance.fs.needToCloseFiles();
if (!needs_close) entry.cache.fd = .fromStdFile(file);
}
const _abs = bun.getFdPath(.fromStdFile(file), &route_file_buf) catch |err| {

View File

@@ -0,0 +1,62 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("Bun.build works multiple times after FileSystemRouter is created", async () => {
using dir = tempDir("issue-18242", {
"pages/index.ts": `console.log("Hello via Bun!");`,
"build.ts": `
import path from "path";
const PAGES_DIR = path.resolve(process.cwd(), "pages");
const srcRouter = new Bun.FileSystemRouter({
dir: PAGES_DIR,
style: "nextjs",
});
const entrypoints = Object.values(srcRouter.routes);
const result1 = await Bun.build({
entrypoints,
outdir: "dist/browser",
});
const result2 = await Bun.build({
entrypoints,
outdir: "dist/bun",
target: "bun",
});
const result3 = await Bun.build({
entrypoints,
outdir: "dist/third",
});
console.log(JSON.stringify({
build1: result1.success,
build2: result2.success,
build3: result3.success,
build2Logs: result2.logs.map(String),
build3Logs: result3.logs.map(String),
}));
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build.ts"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const result = JSON.parse(stdout.trim());
expect(result.build1).toBe(true);
expect(result.build2).toBe(true);
expect(result.build3).toBe(true);
expect(result.build2Logs).toEqual([]);
expect(result.build3Logs).toEqual([]);
expect(exitCode).toBe(0);
});