-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Add FileRoute for serving files #20198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Updated 8:25 PM PT - Jun 10th, 2025
❌ @dylan-conway, your commit e181163 has 3 failures in
🧪 To try this PR locally: bunx bun-pr 20198 That installs a local version of the PR into your bun-20198 --bun |
also don't forget to update types bun/packages/bun-types/bun.d.ts Line 3298 in f62940b
|
Good new 👍 |
value: u64 = 0, | ||
|
||
last_modified_u64: u64 = 0, | ||
last_modified_buffer: [32]u8 = undefined, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The field last_modified_buffer
is initialized with undefined
, which violates the project's rule against defaulting struct fields to undefined
. This can lead to undefined behavior if the buffer is accessed before being properly initialized. Consider using a zero-initialized array (e.g., = [_]u8{0} ** 32
) or ensure the buffer is always initialized before use. The ban-words test has been updated to allow one more occurrence of this pattern, but fixing the underlying issue would be preferable to incrementing the limit.
last_modified_buffer: [32]u8 = undefined, | |
last_modified_buffer: [32]u8 = [_]u8{0} ** 32, |
Spotted by Diamond (based on custom rules)
Is this helpful? React 👍 or 👎 to let us know.
if (prev != this.value and bun.S.ISREG(@intCast(stat.mode))) { | ||
const mtime_timespec = stat.mtime(); | ||
// Clamp negative values to 0 to avoid timestamp overflow issues on Windows | ||
const mtime = bun.timespec{ | ||
.nsec = @intCast(@max(mtime_timespec.nsec, 0)), | ||
.sec = @intCast(@max(mtime_timespec.sec, 0)), | ||
}; | ||
if (mtime.ms() > 0) { | ||
this.last_modified_buffer_len = @intCast(bun.JSC.wtf.writeHTTPDate(&this.last_modified_buffer, mtime.msUnsigned()).len); | ||
this.last_modified_u64 = mtime.msUnsigned(); | ||
} else { | ||
this.last_modified_buffer_len = 0; | ||
this.last_modified_u64 = 0; | ||
} | ||
} else if (!bun.S.ISREG(@intCast(stat.mode))) { | ||
this.last_modified_buffer_len = 0; | ||
this.last_modified_u64 = 0; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation only updates last_modified
values when both the hash changes AND the file is a regular file. This could miss setting these values on the first hash calculation when prev == 0
.
Consider restructuring to:
if (bun.S.ISREG(@intCast(stat.mode))) {
const mtime_timespec = stat.mtime();
// Clamp negative values to 0 to avoid timestamp overflow issues on Windows
const mtime = bun.timespec{
.nsec = @intCast(@max(mtime_timespec.nsec, 0)),
.sec = @intCast(@max(mtime_timespec.sec, 0)),
};
if (mtime.ms() > 0) {
this.last_modified_buffer_len = @intCast(bun.JSC.wtf.writeHTTPDate(&this.last_modified_buffer, mtime.msUnsigned()).len);
this.last_modified_u64 = mtime.msUnsigned();
} else {
this.last_modified_buffer_len = 0;
this.last_modified_u64 = 0;
}
} else {
this.last_modified_buffer_len = 0;
this.last_modified_u64 = 0;
}
This ensures the values are always set for regular files, with the hash check only used as an optimization if needed.
if (prev != this.value and bun.S.ISREG(@intCast(stat.mode))) { | |
const mtime_timespec = stat.mtime(); | |
// Clamp negative values to 0 to avoid timestamp overflow issues on Windows | |
const mtime = bun.timespec{ | |
.nsec = @intCast(@max(mtime_timespec.nsec, 0)), | |
.sec = @intCast(@max(mtime_timespec.sec, 0)), | |
}; | |
if (mtime.ms() > 0) { | |
this.last_modified_buffer_len = @intCast(bun.JSC.wtf.writeHTTPDate(&this.last_modified_buffer, mtime.msUnsigned()).len); | |
this.last_modified_u64 = mtime.msUnsigned(); | |
} else { | |
this.last_modified_buffer_len = 0; | |
this.last_modified_u64 = 0; | |
} | |
} else if (!bun.S.ISREG(@intCast(stat.mode))) { | |
this.last_modified_buffer_len = 0; | |
this.last_modified_u64 = 0; | |
} | |
if (bun.S.ISREG(@intCast(stat.mode))) { | |
const mtime_timespec = stat.mtime(); | |
// Clamp negative values to 0 to avoid timestamp overflow issues on Windows | |
const mtime = bun.timespec{ | |
.nsec = @intCast(@max(mtime_timespec.nsec, 0)), | |
.sec = @intCast(@max(mtime_timespec.sec, 0)), | |
}; | |
if (mtime.ms() > 0) { | |
this.last_modified_buffer_len = @intCast(bun.JSC.wtf.writeHTTPDate(&this.last_modified_buffer, mtime.msUnsigned()).len); | |
this.last_modified_u64 = mtime.msUnsigned(); | |
} else { | |
this.last_modified_buffer_len = 0; | |
this.last_modified_u64 = 0; | |
} | |
} else { | |
this.last_modified_buffer_len = 0; | |
this.last_modified_u64 = 0; | |
} |
Spotted by Diamond
Is this helpful? React 👍 or 👎 to let us know.
return snprintf(buffer, length, "%.3s, %.2u %.3s %.4u %.2u:%.2u:%.2u GMT", | ||
wday_name[tstruct.tm_wday], | ||
tstruct.tm_mday % 99, | ||
mon_name[tstruct.tm_mon], | ||
(1900 + tstruct.tm_year) % 9999, | ||
tstruct.tm_hour % 99, | ||
tstruct.tm_min % 99, | ||
tstruct.tm_sec % 99); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The snprintf
format specifiers use %.2u
which expects values between 0-99, but the modulo operations use % 99
which allows values up to 98. For consistency and to prevent potential issues, either:
- Change the modulo operations to match the format width:
tstruct.tm_mday % 100
for%.2u
, or - Use
%02u
format specifiers and remove the modulo operations entirely sincetm
struct fields are already properly bounded
The second approach is preferred as it's cleaner and relies on the standard guarantees of the tm
struct.
Spotted by Diamond
Is this helpful? React 👍 or 👎 to let us know.
response.body.value = .{ .Blob = blob.dupe() }; | ||
const headers = Headers.from(response.init.headers, bun.default_allocator, .{ .body = &.{ .Blob = blob } }) catch bun.outOfMemory(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a potential memory leak in this code. The blob is duplicated with blob.dupe()
and assigned to response.body.value
before Headers.from()
is called. If Headers.from()
fails with any error other than out-of-memory, the duplicated blob won't be properly cleaned up.
Consider either:
- Moving the blob duplication after the headers creation, or
- Using a temporary variable and ensuring proper cleanup in case of failure
// Option 1: Move duplication after headers creation
const headers = Headers.from(response.init.headers, bun.default_allocator, .{ .body = &.{ .Blob = blob } }) catch bun.outOfMemory();
response.body.value = .{ .Blob = blob.dupe() };
// Option 2: Use temporary and ensure cleanup
var duped_blob = blob.dupe();
const headers = Headers.from(response.init.headers, bun.default_allocator, .{ .body = &.{ .Blob = blob } }) catch |err| {
duped_blob.deinit();
return bun.outOfMemory();
};
response.body.value = .{ .Blob = duped_blob };
response.body.value = .{ .Blob = blob.dupe() }; | |
const headers = Headers.from(response.init.headers, bun.default_allocator, .{ .body = &.{ .Blob = blob } }) catch bun.outOfMemory(); | |
const headers = Headers.from(response.init.headers, bun.default_allocator, .{ .body = &.{ .Blob = blob } }) catch bun.outOfMemory(); | |
response.body.value = .{ .Blob = blob.dupe() }; |
Spotted by Diamond
Is this helpful? React 👍 or 👎 to let us know.
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
src/bun.js/api/server/FileRoute.zig
Outdated
if (fd_result == .err) { | ||
req.setYield(true); | ||
this.deref(); | ||
return; | ||
} | ||
|
||
const fd = fd_result.result.makeLibUVOwned() catch { | ||
req.setYield(true); | ||
this.deref(); | ||
return; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code correctly handles errors from openA
but has a potential file descriptor leak when makeLibUVOwned
fails. In this case, the file descriptor obtained from fd_result.result
should be closed before returning. Consider adding a cleanup step:
const fd = fd_result.result.makeLibUVOwned() catch {
// Close the raw file descriptor to prevent leaks
bun.sys.close(fd_result.result);
req.setYield(true);
this.deref();
return;
};
This ensures proper resource cleanup in all error paths.
if (fd_result == .err) { | |
req.setYield(true); | |
this.deref(); | |
return; | |
} | |
const fd = fd_result.result.makeLibUVOwned() catch { | |
req.setYield(true); | |
this.deref(); | |
return; | |
}; | |
if (fd_result == .err) { | |
req.setYield(true); | |
this.deref(); | |
return; | |
} | |
const fd = fd_result.result.makeLibUVOwned() catch { | |
// Close the raw file descriptor to prevent leaks | |
bun.sys.close(fd_result.result); | |
req.setYield(true); | |
this.deref(); | |
return; | |
}; | |
Spotted by Diamond
Is this helpful? React 👍 or 👎 to let us know.
pub fn initFromBlob(blob: Blob, opts: InitOptions) *FileRoute { | ||
const headers = Headers.from(null, bun.default_allocator, .{ .body = &.{ .Blob = blob } }) catch bun.outOfMemory(); | ||
return bun.new(FileRoute, .{ | ||
.ref_count = .init(), | ||
.server = opts.server, | ||
.blob = blob, | ||
.headers = headers, | ||
.status_code = opts.status_code, | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The has_last_modified_header
and has_content_length_header
fields are uninitialized in this constructor, which could lead to undefined behavior when these fields are accessed later. Consider adding these initializations to the struct:
return bun.new(FileRoute, .{
.ref_count = .init(),
.server = opts.server,
.blob = blob,
.headers = headers,
.status_code = opts.status_code,
.has_last_modified_header = headers.get("last-modified") != null,
.has_content_length_header = headers.get("content-length") != null,
});
This ensures the fields are properly initialized with the correct values based on the headers.
pub fn initFromBlob(blob: Blob, opts: InitOptions) *FileRoute { | |
const headers = Headers.from(null, bun.default_allocator, .{ .body = &.{ .Blob = blob } }) catch bun.outOfMemory(); | |
return bun.new(FileRoute, .{ | |
.ref_count = .init(), | |
.server = opts.server, | |
.blob = blob, | |
.headers = headers, | |
.status_code = opts.status_code, | |
}); | |
} | |
pub fn initFromBlob(blob: Blob, opts: InitOptions) *FileRoute { | |
const headers = Headers.from(null, bun.default_allocator, .{ .body = &.{ .Blob = blob } }) catch bun.outOfMemory(); | |
return bun.new(FileRoute, .{ | |
.ref_count = .init(), | |
.server = opts.server, | |
.blob = blob, | |
.headers = headers, | |
.status_code = opts.status_code, | |
.has_last_modified_header = headers.get("last-modified") != null, | |
.has_content_length_header = headers.get("content-length") != null, | |
}); | |
} | |
Spotted by Diamond
Is this helpful? React 👍 or 👎 to let us know.
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
Summary
FileRoute
for serving filesBun.file()
orResponse
with file bodyFileRoute
intoAnyRoute
uws.AnyResponse
fixes support
Bun.file
on static routes #17362Testing
bun bd test internal/ban-words.test.ts
(fails: missing WebKit)https://chatgpt.com/codex/tasks/task_e_6840cce283088328bef19cdb97982d22