fix(windows): keep event loop alive during Bun.file() async reads

ReadFileUV was not calling refConcurrently()/unrefConcurrently() on the
event loop, causing the process to exit prematurely before the async libuv
file read operations completed. This resulted in Bun.file().text() silently
failing (not throwing ENOENT) when reading non-existent files on Windows.

Fixes #26632

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-01-31 14:49:51 +00:00
parent 71ce550cfa
commit 8d68ebfa2a
3 changed files with 69 additions and 4 deletions

View File

@@ -138,7 +138,7 @@ pub fn doReadFile(this: *Blob, comptime Function: anytype, global: *JSGlobalObje
promise_value.ensureStillAlive(); promise_value.ensureStillAlive();
handler.promise.strong.set(global, promise_value); handler.promise.strong.set(global, promise_value);
read_file.ReadFileUV.start(handler.globalThis.bunVM().uvLoop(), this.store.?, this.offset, this.size, Handler, handler); read_file.ReadFileUV.start(handler.globalThis.bunVM().eventLoop(), this.store.?, this.offset, this.size, Handler, handler);
return promise_value; return promise_value;
} }
@@ -180,7 +180,7 @@ pub fn NewInternalReadFileHandler(comptime Context: type, comptime Function: any
pub fn doReadFileInternal(this: *Blob, comptime Handler: type, ctx: Handler, comptime Function: anytype, global: *JSGlobalObject) void { pub fn doReadFileInternal(this: *Blob, comptime Handler: type, ctx: Handler, comptime Function: anytype, global: *JSGlobalObject) void {
if (Environment.isWindows) { if (Environment.isWindows) {
const ReadFileHandler = NewInternalReadFileHandler(Handler, Function); const ReadFileHandler = NewInternalReadFileHandler(Handler, Function);
return read_file.ReadFileUV.start(libuv.Loop.get(), this.store.?, this.offset, this.size, ReadFileHandler, ctx); return read_file.ReadFileUV.start(global.bunVM().eventLoop(), this.store.?, this.offset, this.size, ReadFileHandler, ctx);
} }
const file_read = read_file.ReadFile.createWithCtx( const file_read = read_file.ReadFile.createWithCtx(
bun.default_allocator, bun.default_allocator,

View File

@@ -523,6 +523,7 @@ pub const ReadFileUV = struct {
pub const doClose = FileCloser(@This()).doClose; pub const doClose = FileCloser(@This()).doClose;
loop: *libuv.Loop, loop: *libuv.Loop,
event_loop: *jsc.EventLoop,
file_store: FileStore, file_store: FileStore,
byte_store: ByteStore = ByteStore{ .allocator = bun.default_allocator }, byte_store: ByteStore = ByteStore{ .allocator = bun.default_allocator },
store: *Store, store: *Store,
@@ -543,10 +544,11 @@ pub const ReadFileUV = struct {
req: libuv.fs_t = std.mem.zeroes(libuv.fs_t), req: libuv.fs_t = std.mem.zeroes(libuv.fs_t),
pub fn start(loop: *libuv.Loop, store: *Store, off: SizeType, max_len: SizeType, comptime Handler: type, handler: *anyopaque) void { pub fn start(event_loop: *jsc.EventLoop, store: *Store, off: SizeType, max_len: SizeType, comptime Handler: type, handler: *anyopaque) void {
log("ReadFileUV.start", .{}); log("ReadFileUV.start", .{});
var this = bun.new(ReadFileUV, .{ var this = bun.new(ReadFileUV, .{
.loop = loop, .loop = event_loop.virtual_machine.uvLoop(),
.event_loop = event_loop,
.file_store = store.data.file, .file_store = store.data.file,
.store = store, .store = store,
.offset = off, .offset = off,
@@ -555,15 +557,20 @@ pub const ReadFileUV = struct {
.on_complete_fn = @ptrCast(&Handler.run), .on_complete_fn = @ptrCast(&Handler.run),
}); });
store.ref(); store.ref();
// Keep the event loop alive while the async operation is pending
event_loop.refConcurrently();
this.getFd(onFileOpen); this.getFd(onFileOpen);
} }
pub fn finalize(this: *ReadFileUV) void { pub fn finalize(this: *ReadFileUV) void {
log("ReadFileUV.finalize", .{}); log("ReadFileUV.finalize", .{});
const event_loop = this.event_loop;
defer { defer {
this.store.deref(); this.store.deref();
this.req.deinit(); this.req.deinit();
bun.destroy(this); bun.destroy(this);
// Release the event loop reference now that we're done
event_loop.unrefConcurrently();
log("ReadFileUV.finalize destroy", .{}); log("ReadFileUV.finalize destroy", .{});
} }

View File

@@ -0,0 +1,58 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// https://github.com/oven-sh/bun/issues/26632
// Bun.file().text() on a non-existent file should throw ENOENT error, not silently exit
test("Bun.file().text() on nonexistent file throws ENOENT", async () => {
using dir = tempDir("26632", {});
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `await Bun.file("nonexistent-file-that-does-not-exist.txt").text();`],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toContain("ENOENT");
expect(exitCode).not.toBe(0);
});
test("Bun.file().arrayBuffer() on nonexistent file throws ENOENT", async () => {
using dir = tempDir("26632", {});
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `await Bun.file("nonexistent-file-that-does-not-exist.txt").arrayBuffer();`],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toContain("ENOENT");
expect(exitCode).not.toBe(0);
});
test("Bun.file().bytes() on nonexistent file throws ENOENT", async () => {
using dir = tempDir("26632", {});
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `await Bun.file("nonexistent-file-that-does-not-exist.txt").bytes();`],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toContain("ENOENT");
expect(exitCode).not.toBe(0);
});