Skip to content

Commit

Permalink
[Bun.serve] Implement Content-Range support with Bun.file()
Browse files Browse the repository at this point in the history
  • Loading branch information
Jarred-Sumner committed Dec 4, 2022
1 parent 1c3cb22 commit 0617896
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 16 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2118,6 +2118,22 @@ Bun.serve({
});
```
### Sending files with Bun.serve()
`Bun.serve()` lets you send files fast.
To send a file, return a `Response` object with a `Bun.file(pathOrFd)` object as the body.
```ts
Bun.serve({
fetch(req) {
return new Response(Bun.file("./hello.txt"));
},
});
```
Under the hood, when TLS is not enabled, Bun automatically uses the sendfile(2) system call. This enables zero-copy file transfers, which is faster than reading the file into memory and sending it.
### WebSockets with Bun.serve()
`Bun.serve()` has builtin support for server-side websockets (as of Bun v0.2.1).
Expand Down
120 changes: 107 additions & 13 deletions src/bun.js/api/server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
has_sendfile_ctx: bool = false,
has_called_error_handler: bool = false,
needs_content_length: bool = false,
needs_content_range: bool = false,
sendfile: SendfileContext = undefined,
request_js_object: JSC.C.JSObjectRef = null,
request_body_buf: std.ArrayListUnmanaged(u8) = .{},
Expand Down Expand Up @@ -883,6 +884,28 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
return false;
}

// TODO: should we cork?
pub fn onWritableCompleteResponseBufferAndMetadata(this: *RequestContext, write_offset: c_ulong, resp: *App.Response) callconv(.C) bool {
std.debug.assert(this.resp == resp);

if (this.aborted) {
this.finalizeForAbort();
return false;
}

if (!this.has_written_status) {
this.renderMetadata();
}

if (this.method == .HEAD) {
resp.end("", this.shouldCloseConnection());
this.finalize();
return false;
}

return this.sendWritableBytesForCompleteResponseBuffer(this.response_buf_owned.items, write_offset, resp);
}

pub fn onWritableCompleteResponseBuffer(this: *RequestContext, write_offset: c_ulong, resp: *App.Response) callconv(.C) bool {
std.debug.assert(this.resp == resp);
if (this.aborted) {
Expand Down Expand Up @@ -1119,7 +1142,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp

const errcode = linux.getErrno(val);

this.sendfile.remain -= @intCast(Blob.SizeType, this.sendfile.offset - start);
this.sendfile.remain -|= @intCast(Blob.SizeType, this.sendfile.offset -| start);

if (errcode != .SUCCESS or this.aborted or this.sendfile.remain == 0 or val == 0) {
if (errcode != .AGAIN and errcode != .SUCCESS and errcode != .PIPE) {
Expand All @@ -1132,7 +1155,6 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
} else {
var sbytes: std.os.off_t = adjusted_count;
const signed_offset = @bitCast(i64, @as(u64, this.sendfile.offset));

const errcode = std.c.getErrno(std.c.sendfile(
this.sendfile.fd,
this.sendfile.socket_fd,
Expand All @@ -1143,8 +1165,8 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
0,
));
const wrote = @intCast(Blob.SizeType, sbytes);
this.sendfile.offset += wrote;
this.sendfile.remain -= wrote;
this.sendfile.offset +|= wrote;
this.sendfile.remain -|= wrote;
if (errcode != .AGAIN or this.aborted or this.sendfile.remain == 0 or sbytes == 0) {
if (errcode != .AGAIN and errcode != .SUCCESS and errcode != .PIPE) {
Output.prettyErrorln("Error: {s}", .{@tagName(errcode)});
Expand Down Expand Up @@ -1280,19 +1302,39 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
}
}

this.blob.Blob.size = @intCast(Blob.SizeType, stat.size);
const original_size = this.blob.Blob.size;
const stat_size = @intCast(Blob.SizeType, stat.size);
this.blob.Blob.size = if (std.os.S.ISREG(stat.mode))
stat_size
else
@minimum(original_size, stat_size);

this.needs_content_length = true;

this.sendfile = .{
.fd = fd,
.remain = this.blob.Blob.size,
.remain = this.blob.Blob.offset + original_size,
.offset = this.blob.Blob.offset,
.auto_close = auto_close,
.socket_fd = if (!this.aborted) this.resp.getNativeHandle() else -999,
};

// if we are sending only part of a file, include the content-range header
// only include content-range automatically when using a file path instead of an fd
// this is to better support manually controlling the behavior
if (std.os.S.ISREG(stat.mode) and auto_close) {
this.needs_content_range = (this.sendfile.remain -| this.sendfile.offset) != stat_size;
}

// we know the bounds when we are sending a regular file
if (std.os.S.ISREG(stat.mode)) {
this.sendfile.offset = @minimum(this.sendfile.offset, stat_size);
this.sendfile.remain = @minimum(@maximum(this.sendfile.remain, this.sendfile.offset), stat_size) -| this.sendfile.offset;
}

this.resp.runCorkedWithType(*RequestContext, renderMetadataAndNewline, this);

if (this.blob.Blob.size == 0) {
if (this.sendfile.remain == 0 or !this.method.hasBody()) {
this.cleanupAndFinalizeAfterSendfile();
return;
}
Expand Down Expand Up @@ -1339,9 +1381,28 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
this.blob.Blob.resolveSize();
this.doRenderBlob();
} else {
this.blob.Blob.size = @truncate(Blob.SizeType, result.result.buf.len);
const stat_size = @intCast(Blob.SizeType, result.result.total_size);
const original_size = this.blob.Blob.size;

this.blob.Blob.size = if (original_size == 0 or original_size == Blob.max_size)
stat_size
else
@minimum(original_size, stat_size);

if (!this.has_written_status)
this.needs_content_range = true;

// this is used by content-range
this.sendfile = .{
.fd = @truncate(i32, bun.invalid_fd),
.remain = @truncate(Blob.SizeType, result.result.buf.len),
.offset = this.blob.Blob.offset,
.auto_close = false,
.socket_fd = -999,
};

this.response_buf_owned = .{ .items = result.result.buf, .capacity = result.result.buf.len };
this.resp.onWritable(*RequestContext, onWritableCompleteResponseBuffer, this);
this.resp.onWritable(*RequestContext, onWritableCompleteResponseBufferAndMetadata, this);
}
}

Expand Down Expand Up @@ -2078,13 +2139,18 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
pub fn renderMetadata(this: *RequestContext) void {
var response: *JSC.WebCore.Response = this.response_ptr.?;
var status = response.statusCode();
const size = this.blob.size();
var needs_content_range = this.needs_content_range;

const size = if (needs_content_range)
this.sendfile.remain
else
this.blob.size();

status = if (status == 200 and size == 0 and !this.blob.isDetached())
204
else
status;

this.writeStatus(status);
var needs_content_type = true;
const content_type: MimeType = brk: {
if (response.body.init.headers) |headers_| {
Expand All @@ -2105,12 +2171,23 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
};

var has_content_disposition = false;

if (response.body.init.headers) |headers_| {
this.writeHeaders(headers_);
has_content_disposition = headers_.fastHas(.ContentDisposition);
needs_content_range = needs_content_range and headers_.fastHas(.ContentRange);
if (needs_content_range) {
status = 206;
}

this.writeStatus(status);
this.writeHeaders(headers_);

response.body.init.headers = null;
headers_.deref();
} else if (needs_content_range) {
status = 206;
this.writeStatus(status);
} else {
this.writeStatus(status);
}

if (needs_content_type and
Expand Down Expand Up @@ -2146,6 +2223,23 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
this.resp.writeHeaderInt("content-length", size);
this.needs_content_length = false;
}

if (needs_content_range) {
var content_range_buf: [1024]u8 = undefined;

this.resp.writeHeader(
"content-range",
std.fmt.bufPrint(
&content_range_buf,
// we omit the full size of the Blob because it could
// change between requests and this potentially leaks
// PII undesirably
"bytes {d}-{d}/*",
.{ this.sendfile.offset, this.sendfile.offset + this.sendfile.remain },
) catch "bytes */*",
);
this.needs_content_range = false;
}
}

pub fn renderBytes(this: *RequestContext) void {
Expand Down
5 changes: 4 additions & 1 deletion src/bun.js/webcore/response.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2010,6 +2010,7 @@ pub const Blob = struct {
pub const Read = struct {
buf: []u8,
is_temporary: bool = false,
total_size: SizeType = 0,
};
pub const ResultType = SystemError.Maybe(Read);

Expand Down Expand Up @@ -2105,7 +2106,7 @@ pub const Blob = struct {
return;
}

cb(cb_ctx, .{ .result = .{ .buf = buf, .is_temporary = true } });
cb(cb_ctx, .{ .result = .{ .buf = buf, .total_size = this.size, .is_temporary = true } });
}
pub fn run(this: *ReadFile, task: *ReadFileTask) void {
this.runAsync(task);
Expand Down Expand Up @@ -2154,6 +2155,8 @@ pub const Blob = struct {
const file = &this.file_store;
const needs_close = fd != null_fd and file.pathlike == .path and fd > 2;

this.size = @maximum(this.read_len, this.size);

if (needs_close) {
this.doClose();
}
Expand Down
1 change: 0 additions & 1 deletion src/bun.js/webcore/streams.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4445,7 +4445,6 @@ pub const FileReader = struct {
return .{ .owned_and_done = this.drainInternalBuffer() };
}


return this.readable().read(buffer, view, this.globalThis());
}

Expand Down
Loading

0 comments on commit 0617896

Please sign in to comment.