Skip to content

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

Merged
merged 39 commits into from
Jun 11, 2025

Conversation

Jarred-Sumner
Copy link
Collaborator

@Jarred-Sumner Jarred-Sumner commented Jun 5, 2025

Summary

  • implement FileRoute for serving files
  • support parsing Bun.file() or Response with file body
  • integrate FileRoute into AnyRoute
  • handle sendfile responses through uws.AnyResponse
    fixes support Bun.file on static routes #17362

Testing

  • bun bd test internal/ban-words.test.ts (fails: missing WebKit)

https://chatgpt.com/codex/tasks/task_e_6840cce283088328bef19cdb97982d22

@robobun
Copy link

robobun commented Jun 5, 2025

Updated 8:25 PM PT - Jun 10th, 2025

@dylan-conway, your commit e181163 has 3 failures in Build #18466:


🧪   To try this PR locally:

bunx bun-pr 20198

That installs a local version of the PR into your bun-20198 executable, so you can run:

bun-20198 --bun

@RiskyMH
Copy link
Member

RiskyMH commented Jun 5, 2025

also don't forget to update types

type RouteValue<T extends string> = Response | false | RouteHandler<T> | RouteHandlerObject<T>;

@TiBianMod
Copy link

Good new 👍
@Jarred-Sumner, also related to routes #17363
will be nice addition

@Jarred-Sumner Jarred-Sumner marked this pull request as ready for review June 6, 2025 00:15
value: u64 = 0,

last_modified_u64: u64 = 0,
last_modified_buffer: [32]u8 = undefined,
Copy link
Contributor

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.

Suggested change
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.

Comment on lines +20 to +37
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;
}
Copy link
Contributor

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.

Suggested change
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.

Comment on lines +1010 to +1017
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);
Copy link
Contributor

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:

  1. Change the modulo operations to match the format width: tstruct.tm_mday % 100 for %.2u, or
  2. Use %02u format specifiers and remove the modulo operations entirely since tm 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.

Comment on lines +69 to +70
response.body.value = .{ .Blob = blob.dupe() };
const headers = Headers.from(response.init.headers, bun.default_allocator, .{ .body = &.{ .Blob = blob } }) catch bun.outOfMemory();
Copy link
Contributor

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:

  1. Moving the blob duplication after the headers creation, or
  2. 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 };
Suggested change
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.

Comment on lines 163 to 173
if (fd_result == .err) {
req.setYield(true);
this.deref();
return;
}

const fd = fd_result.result.makeLibUVOwned() catch {
req.setYield(true);
this.deref();
return;
};
Copy link
Contributor

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.

Suggested change
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.

Comment on lines +36 to +45
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,
});
}
Copy link
Contributor

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.

Suggested change
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.

dylan-conway and others added 2 commits June 10, 2025 16:30
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
@dylan-conway dylan-conway merged commit 8750f0b into main Jun 11, 2025
5 of 7 checks passed
@dylan-conway dylan-conway deleted the codex/implement-fileroute-for-bun.serve branch June 11, 2025 02:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

support Bun.file on static routes
6 participants