Skip to content
Open
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
16 changes: 14 additions & 2 deletions src/runtime/api/filesystem_router.zig
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,22 @@ pub const FileSystemRouter = struct {
return globalThis.throw("Unable to find directory: {s}", .{this.router.config.dir});
};

// Deep-copy config strings into the new arena. The previous arena is freed below, so
// any slice that still points into it (the inner extension strings, asset_prefix_path)
// would dangle on the *next* reload() and be read by loadRoutes. dupe(string, ...) only
// copies the outer []string; each inner []const u8 must be duped individually.
const extensions = extensions: {
const old = this.router.config.extensions;
const new = allocator.alloc(string, old.len) catch unreachable;
for (old, new) |ext, *out| {
out.* = allocator.dupe(u8, ext) catch unreachable;
}
break :extensions new;
};
var router = Router.init(vm.transpiler.fs, allocator, .{
.dir = allocator.dupe(u8, this.router.config.dir) catch unreachable,
.extensions = allocator.dupe(string, this.router.config.extensions) catch unreachable,
.asset_prefix_path = this.router.config.asset_prefix_path,
.extensions = extensions,
.asset_prefix_path = allocator.dupe(u8, this.router.config.asset_prefix_path) catch unreachable,
}) catch unreachable;
router.loadRoutes(&log, root_dir_info, Resolver, &vm.transpiler.resolver, router.config.dir) catch {
// Build the JS error before freeing the arena: `log` is backed by the arena allocator.
Expand Down
35 changes: 35 additions & 0 deletions test/js/bun/util/filesystem_router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,41 @@ it("reload() works", () => {
expect(router.match("/posts")!.name).toBe("/posts");
});

it("reload() preserves custom fileExtensions and assetPrefix across multiple reloads", () => {
// Regression: reload() shallow-copied the []string of extensions into the new arena but
// left the inner []const u8 pointing into the old arena (which is then freed). The second
// reload() would scan routes against dangling extension bytes, dropping every route (and
// tripping ASAN). asset_prefix_path was not copied at all.
const { dir } = make(["index.tsx", "posts/[id].tsx", "posts.tsx"]);

const router = new Bun.FileSystemRouter({
dir,
style: "nextjs",
fileExtensions: [".tsx"],
assetPrefix: "/_next/static/",
origin: "https://nextjs.org",
});

const expected = {
"/": `${dir}/index.tsx`,
"/posts": `${dir}/posts.tsx`,
"/posts/[id]": `${dir}/posts/[id].tsx`,
};

expect(router.routes).toEqual(expected);

// First reload() happens while the original arena is still live during loadRoutes,
// so it appears to work even with the shallow copy. The second and subsequent reloads
// read extension strings that were freed by the previous reload.
for (let i = 0; i < 5; i++) {
router.reload();
expect(router.routes).toEqual(expected);
const { name, src } = router.match("/posts/hello-world")!;
expect(name).toBe("/posts/[id]");
expect(src).toBe("https://nextjs.org/_next/static/posts/[id].tsx");
}
});

it("reload() works with new dirs/files", () => {
const { dir } = make(["posts.tsx"]);

Expand Down
Loading