diff options
author | 2023-04-28 17:40:44 -0300 | |
---|---|---|
committer | 2023-04-28 13:40:44 -0700 | |
commit | 4515a6373e22246e2ec6df4475dc63a965bf9c4a (patch) | |
tree | b7a3dd436ecbbef454468ed71a7583ee590e16f7 | |
parent | c383638ddd0b220c482e2e0ec7baa2f650d05776 (diff) | |
download | bun-4515a6373e22246e2ec6df4475dc63a965bf9c4a.tar.gz bun-4515a6373e22246e2ec6df4475dc63a965bf9c4a.tar.zst bun-4515a6373e22246e2ec6df4475dc63a965bf9c4a.zip |
Improves Body.Value life cycle and Signal life cycle on server.zig (#2752)
* reestruct request body value and signal
* revert react-hello-world
* fix constructInto and test
* fmt
* fix body nullable
* Request can outlive RequestContext
* fmt
* BodyValue is now HiveRef
* hasDecl for Ref and HiveRef
* fix deinit call on Ref/HiveRef
* adds InlineBlob
* fix Bun.inspect when using InlineBlob
* revert InlineBlob
* Fix mimalloc-debug
* Add TODO note
* fix: make node:crypto Hash.copy work correctly (#2761)
This commit will also:
- add a CryptoHasher.copy function
- make CryptoHasher.digest reset the hasher so it can be reused
Resolves #2651
* :nail_care:
* address unicode issue (#2763)
* Fix an oopsie
* Another oopsie
* use inline for
* Fixup
---------
Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
* Request can outlive RequestContext
* fmt
* garantee to have the abort signnal attached to the server before abort the client on bun-server test
---------
Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
Co-authored-by: Silver <14016168+silversquirl@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
-rw-r--r-- | src/bun.js/api/bun/socket.zig | 4 | ||||
-rw-r--r-- | src/bun.js/api/server.zig | 286 | ||||
-rw-r--r-- | src/bun.js/webcore/blob.zig | 17 | ||||
-rw-r--r-- | src/bun.js/webcore/body.zig | 7 | ||||
-rw-r--r-- | src/bun.js/webcore/request.zig | 99 | ||||
-rw-r--r-- | src/bun.js/webcore/response.zig | 16 | ||||
-rw-r--r-- | src/bun.zig | 68 | ||||
-rw-r--r-- | src/hive_array.zig | 8 | ||||
-rw-r--r-- | src/js_ast.zig | 2 | ||||
-rw-r--r-- | test/js/bun/http/app.jsx | 10 | ||||
-rw-r--r-- | test/js/bun/http/bun-server.test.ts | 11 | ||||
-rw-r--r-- | test/js/bun/http/serve.test.ts | 30 |
12 files changed, 331 insertions, 227 deletions
diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 5980de6ec..8b78cf035 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -1476,8 +1476,8 @@ fn NewSocket(comptime ssl: bool) type { globalObject.throw("Only Blob/buffered bodies are supported for now", .{}); return .{ .fail = {} }; } else if (args.ptr[0].as(JSC.WebCore.Request)) |request| { - request.body.toBlobIfPossible(); - if (request.body.tryUseAsAnyBlob()) |blob| { + request.body.value.toBlobIfPossible(); + if (request.body.value.tryUseAsAnyBlob()) |blob| { break :getter blob; } diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 4662ad0bc..f8be42e79 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -941,7 +941,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp needs_content_length: bool = false, needs_content_range: bool = false, sendfile: SendfileContext = undefined, - request_js_object: JSC.C.JSObjectRef = null, + request_body: ?*JSC.WebCore.BodyValueRef = null, request_body_buf: std.ArrayListUnmanaged(u8) = .{}, request_body_content_len: usize = 0, @@ -983,17 +983,6 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp const result = arguments.ptr[0]; result.ensureStillAlive(); - if (ctx.request_js_object != null and ctx.signal == null) { - var request_js = ctx.request_js_object.?.value(); - request_js.ensureStillAlive(); - if (request_js.as(Request)) |request_object| { - if (request_object.signal) |signal| { - ctx.signal = signal; - _ = signal.ref(); - } - } - } - ctx.pending_promises_for_abort -|= 1; if (ctx.aborted) { ctx.finalizeForAbort(); @@ -1042,17 +1031,6 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp var ctx = arguments.ptr[1].asPromisePtr(@This()); const err = arguments.ptr[0]; - if (ctx.request_js_object != null and ctx.signal == null) { - var request_js = ctx.request_js_object.?.value(); - request_js.ensureStillAlive(); - if (request_js.as(Request)) |request_object| { - if (request_object.signal) |signal| { - ctx.signal = signal; - _ = signal.ref(); - } - } - } - ctx.pending_promises_for_abort -|= 1; if (ctx.aborted) { @@ -1273,11 +1251,9 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp return false; } - if (this.request_js_object) |obj| { - if (obj.value().as(Request)) |req| { - if (req.body == .Locked) { - return false; - } + if (this.request_body) |body| { + if (body.value == .Locked) { + return false; } } @@ -1317,27 +1293,20 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp // if we cannot, we have to reject pending promises // first, we reject the request body promise - if (this.request_js_object != null) { - var request_js = this.request_js_object.?.value(); - request_js.ensureStillAlive(); - - this.request_js_object = null; - defer request_js.ensureStillAlive(); - defer JSC.C.JSValueUnprotect(this.server.globalThis, request_js.asObjectRef()); + if (this.request_body) |body| { // User called .blob(), .json(), text(), or .arrayBuffer() on the Request object // but we received nothing or the connection was aborted - if (request_js.as(Request)) |req| { + if (body.value == .Locked) { // the promise is pending - if (req.body == .Locked and (req.body.Locked.action != .none or req.body.Locked.promise != null)) { + if (body.value.Locked.action != .none or body.value.Locked.promise != null) { this.pending_promises_for_abort += 1; - req.body.toErrorInstance(JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis), this.server.globalThis); - } else if (req.body == .Locked and (req.body.Locked.readable != null)) { - req.body.Locked.readable.?.abort(this.server.globalThis); - req.body.toErrorInstance(JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis), this.server.globalThis); - req.body.Locked.readable = null; + body.value.toErrorInstance(JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis), this.server.globalThis); + } else if (body.value.Locked.readable != null) { + body.value.Locked.readable.?.abort(this.server.globalThis); + body.value.toErrorInstance(JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis), this.server.globalThis); + body.value.Locked.readable = null; } - req.uws_request = null; } } @@ -1399,23 +1368,13 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp _ = signal.unref(); } - if (this.request_js_object != null) { - ctxLog("finalizeWithoutDeinit: request_js_object != null", .{}); - - var request_js = this.request_js_object.?.value(); - request_js.ensureStillAlive(); - - this.request_js_object = null; - defer request_js.ensureStillAlive(); - defer JSC.C.JSValueUnprotect(this.server.globalThis, request_js.asObjectRef()); + if (this.request_body) |body| { + ctxLog("finalizeWithoutDeinit: request_body != null", .{}); // User called .blob(), .json(), text(), or .arrayBuffer() on the Request object // but we received nothing or the connection was aborted - if (request_js.as(Request)) |req| { - // the promise is pending - if (req.body == .Locked and req.body.Locked.action != .none and req.body.Locked.promise != null) { - req.body.toErrorInstance(JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis), this.server.globalThis); - } - req.uws_request = null; + // the promise is pending + if (body.value == .Locked and body.value.Locked.action != .none and body.value.Locked.promise != null) { + body.value.toErrorInstance(JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis), this.server.globalThis); } } @@ -1461,6 +1420,10 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp this.request_body_buf.clearAndFree(this.allocator); this.response_buf_owned.clearAndFree(this.allocator); + if (this.request_body) |body| { + _ = body.unref(); + this.request_body = null; + } server.request_pool_allocator.destroy(this); } @@ -2037,12 +2000,6 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp switch (promise.status(vm.global.vm())) { .Pending => {}, .Fulfilled => { - if (ctx.signal == null) { - if (request_object.signal) |signal| { - ctx.signal = signal; - _ = signal.ref(); - } - } const fulfilled_value = promise.result(vm.global.vm()); // if you return a Response object or a Promise<Response> @@ -2085,12 +2042,6 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp return; }, .Rejected => { - if (ctx.signal == null) { - if (request_object.signal) |signal| { - ctx.signal = signal; - _ = signal.ref(); - } - } ctx.handleReject(promise.result(vm.global.vm())); return; }, @@ -2661,84 +2612,80 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp if (this.aborted) return; - const request = JSC.JSValue.fromRef(this.request_js_object); - var req = request.as(Request) orelse { - this.request_body_buf.clearAndFree(this.allocator); - return; - }; + if (this.request_body != null) { + var body = this.request_body.?; - if (req.body == .Locked) { - if (req.body.Locked.readable) |readable| { - if (readable.ptr == .Bytes) { - std.debug.assert(this.request_body_buf.items.len == 0); + if (body.value == .Locked) { + if (body.value.Locked.readable) |readable| { + if (readable.ptr == .Bytes) { + std.debug.assert(this.request_body_buf.items.len == 0); - if (!last) { - readable.ptr.Bytes.onData( - .{ - .temporary = bun.ByteList.initConst(chunk), - }, - bun.default_allocator, - ); - } else { - readable.ptr.Bytes.onData( - .{ - .temporary_and_done = bun.ByteList.initConst(chunk), - }, - bun.default_allocator, - ); - } + if (!last) { + readable.ptr.Bytes.onData( + .{ + .temporary = bun.ByteList.initConst(chunk), + }, + bun.default_allocator, + ); + } else { + readable.ptr.Bytes.onData( + .{ + .temporary_and_done = bun.ByteList.initConst(chunk), + }, + bun.default_allocator, + ); + } - return; + return; + } } } - } - if (last) { - request.ensureStillAlive(); - var bytes = this.request_body_buf; - defer this.request_body_buf = .{}; - var old = req.body; + if (last) { + var bytes = this.request_body_buf; + defer this.request_body_buf = .{}; + var old = body.value; + + const total = bytes.items.len + chunk.len; + getter: { + // if (total <= JSC.WebCore.InlineBlob.available_bytes) { + // if (total == 0) { + // body.value = .{ .Empty = {} }; + // break :getter; + // } + + // body.value = .{ .InlineBlob = JSC.WebCore.InlineBlob.concat(bytes.items, chunk) }; + // this.request_body_buf.clearAndFree(this.allocator); + // } else { + bytes.ensureTotalCapacityPrecise(this.allocator, total) catch |err| { + this.request_body_buf.clearAndFree(this.allocator); + body.value.toError(err, this.server.globalThis); + break :getter; + }; - const total = bytes.items.len + chunk.len; - getter: { - // if (total <= JSC.WebCore.InlineBlob.available_bytes) { - // if (total == 0) { - // req.body = .{ .Empty = {} }; - // break :getter; - // } - - // req.body = .{ .InlineBlob = JSC.WebCore.InlineBlob.concat(bytes.items, chunk) }; - // this.request_body_buf.clearAndFree(this.allocator); - // } else { - bytes.ensureTotalCapacityPrecise(this.allocator, total) catch |err| { - this.request_body_buf.clearAndFree(this.allocator); - req.body.toError(err, this.server.globalThis); - break :getter; - }; + const prev_len = bytes.items.len; + bytes.items.len = total; + var slice = bytes.items[prev_len..]; + @memcpy(slice.ptr, chunk.ptr, chunk.len); + body.value = .{ + .InternalBlob = .{ + .bytes = bytes.toManaged(this.allocator), + }, + }; + // } + } - const prev_len = bytes.items.len; - bytes.items.len = total; - var slice = bytes.items[prev_len..]; - @memcpy(slice.ptr, chunk.ptr, chunk.len); - req.body = .{ - .InternalBlob = .{ - .bytes = bytes.toManaged(this.allocator), - }, - }; - // } + if (old == .Locked) { + old.resolve(&body.value, this.server.globalThis); + } + return; } - if (old == .Locked) { - old.resolve(&req.body, this.server.globalThis); + if (this.request_body_buf.capacity == 0) { + this.request_body_buf.ensureTotalCapacityPrecise(this.allocator, @min(this.request_body_content_len, max_request_body_preallocate_length)) catch @panic("Out of memory while allocating request body buffer"); } - request.unprotect(); - return; + this.request_body_buf.appendSlice(this.allocator, chunk) catch @panic("Out of memory while allocating request body"); } - - if (this.request_body_buf.capacity == 0) { - this.request_body_buf.ensureTotalCapacityPrecise(this.allocator, @min(this.request_body_content_len, max_request_body_preallocate_length)) catch @panic("Out of memory while allocating request body buffer"); - } - this.request_body_buf.appendSlice(this.allocator, chunk) catch @panic("Out of memory while allocating request body"); } pub fn onStartStreamingRequestBody(this: *RequestContext) JSC.WebCore.DrainResult { @@ -2771,22 +2718,20 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp const max_request_body_preallocate_length = 1024 * 256; pub fn onStartBuffering(this: *RequestContext) void { ctxLog("onStartBuffering", .{}); - const request = JSC.JSValue.c(this.request_js_object); - request.ensureStillAlive(); // TODO: check if is someone calling onStartBuffering other than onStartBufferingCallback // if is not, this should be removed and only keep protect + setAbortHandler if (this.is_transfer_encoding == false and this.request_body_content_len == 0) { // no content-length or 0 content-length // no transfer-encoding - if (request.as(Request)) |req| { - var old = req.body; + if (this.request_body != null) { + var body = this.request_body.?; + var old = body.value; old.Locked.onReceiveValue = null; - req.body = .{ .Null = {} }; - old.resolve(&req.body, this.server.globalThis); + var new_body = .{ .Null = {} }; + old.resolve(&new_body, this.server.globalThis); + body.value = new_body; } } else { - // we need to buffer the request body - request.protect(); this.setAbortHandler(); } } @@ -4201,6 +4146,7 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { config: ServerConfig = ServerConfig{}, pending_requests: usize = 0, request_pool_allocator: std.mem.Allocator = undefined, + listen_callback: JSC.AnyTask = undefined, allocator: std.mem.Allocator, poll_ref: JSC.PollRef = .{}, @@ -4596,7 +4542,7 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { existing_request = Request{ .url = url.href, .headers = headers, - .body = body, + .body = JSC.WebCore.InitRequestBodyValue(body) catch unreachable, .method = method, }; } @@ -4948,15 +4894,22 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { var ctx = this.request_pool_allocator.create(RequestContext) catch @panic("ran out of memory"); ctx.create(this, req, resp); var request_object = this.allocator.create(JSC.WebCore.Request) catch unreachable; + var body = JSC.WebCore.InitRequestBodyValue(.{ .Null = {} }) catch unreachable; + + ctx.request_body = body; + const js_signal = JSC.WebCore.AbortSignal.create(this.globalThis); + js_signal.ensureStillAlive(); + if (JSC.WebCore.AbortSignal.fromJS(js_signal)) |signal| { + ctx.signal = signal.ref().ref(); // +2 refs 1 for the request and 1 for the request context + } request_object.* = .{ .url = "", .method = ctx.method, .uws_request = req, .https = ssl_enabled, - .body = .{ - .Null = {}, - }, + .signal = ctx.signal, + .body = body.ref(), }; if (comptime debug_mode) { @@ -4996,7 +4949,7 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { // we defer pre-allocating the body until we receive the first chunk // that way if the client is lying about how big the body is or the client aborts // we don't waste memory - request_object.body = .{ + ctx.request_body.?.value = .{ .Locked = .{ .task = ctx, .global = this.globalThis, @@ -5005,14 +4958,6 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { }, }; resp.onData(*RequestContext, RequestContext.onBufferedBodyChunk, ctx); - } else if (request_object.body == .Locked) { - // This branch should never be taken, but we are handling it anyway. - var old = request_object.body; - old.Locked.onReceiveValue = null; - request_object.body = .{ .Null = {} }; - old.resolve(&request_object.body, this.globalThis); - } else { - request_object.body = .{ .Null = {} }; } } @@ -5021,15 +4966,12 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { request_object.toJS(this.globalThis), this.thisObject, }; - ctx.request_js_object = args[0].asObjectRef(); + const request_value = args[0]; request_value.ensureStillAlive(); const response_value = this.config.onRequest.callWithThis(this.globalThis, this.thisObject, &args); - if (request_object.signal) |signal| { - ctx.signal = signal; - _ = signal.ref(); - } + ctx.onResponse( this, req, @@ -5037,6 +4979,8 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { request_value, response_value, ); + // uWS request will not live longer than this function + request_object.uws_request = null; } pub fn onWebSocketUpgrade( @@ -5052,15 +4996,23 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { var ctx = this.request_pool_allocator.create(RequestContext) catch @panic("ran out of memory"); ctx.create(this, req, resp); var request_object = this.allocator.create(JSC.WebCore.Request) catch unreachable; + var body = JSC.WebCore.InitRequestBodyValue(.{ .Null = {} }) catch unreachable; + + ctx.request_body = body; + const js_signal = JSC.WebCore.AbortSignal.create(this.globalThis); + js_signal.ensureStillAlive(); + if (JSC.WebCore.AbortSignal.fromJS(js_signal)) |signal| { + ctx.signal = signal.ref().ref(); // +2 refs 1 for the request and 1 for the request context + } + request_object.* = .{ .url = "", .method = ctx.method, .uws_request = req, .upgrader = ctx, .https = ssl_enabled, - .body = .{ - .Null = {}, - }, + .signal = ctx.signal, + .body = body.ref(), }; ctx.upgrade_context = upgrade_ctx; @@ -5069,7 +5021,6 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { request_object.toJS(this.globalThis), this.thisObject, }; - ctx.request_js_object = args[0].asObjectRef(); const request_value = args[0]; request_value.ensureStillAlive(); const response_value = this.config.onRequest.callWithThis(this.globalThis, this.thisObject, &args); @@ -5081,6 +5032,9 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { request_value, response_value, ); + + // uWS request will not live longer than this function + request_object.uws_request = null; } pub fn listen(this: *ThisServer) void { diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index c5e07b13c..5723a36b4 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -711,7 +711,7 @@ pub const Blob = struct { } if (data.as(Request)) |request| { - switch (request.body) { + switch (request.body.value) { // .InlineBlob, .InternalBlob, .Used, @@ -719,13 +719,13 @@ pub const Blob = struct { .Blob, .Null, => { - break :brk request.body.use(); + break :brk request.body.value.use(); }, .Error => { destination_blob.detach(); - const err = request.body.Error; + const err = request.body.value.Error; JSC.C.JSValueUnprotect(ctx, err.asObjectRef()); - _ = request.body.use(); + _ = request.body.value.use(); return JSC.JSPromise.rejectedPromiseValue(ctx.ptr(), err).asObjectRef(); }, .Locked => { @@ -737,8 +737,8 @@ pub const Blob = struct { .promise = promise, }; - request.body.Locked.task = task; - request.body.Locked.onReceiveValue = WriteFileWaitFromLockedValueTask.thenWrap; + request.body.value.Locked.task = task; + request.body.value.Locked.onReceiveValue = WriteFileWaitFromLockedValueTask.thenWrap; return promise.asValue(ctx.ptr()).asObjectRef(); }, @@ -3436,6 +3436,9 @@ pub const AnyBlob = union(enum) { switch (this.*) { .Blob => return this.Blob.toString(global, lifetime), // .InlineBlob => { + // if (this.InlineBlob.len == 0) { + // return ZigString.Empty.toValue(global); + // } // const owned = this.InlineBlob.toStringOwned(global); // this.* = .{ .InlineBlob = .{ .len = 0 } }; // return owned; @@ -3457,7 +3460,7 @@ pub const AnyBlob = union(enum) { .Blob => return this.Blob.toArrayBuffer(global, lifetime), // .InlineBlob => { // if (this.InlineBlob.len == 0) { - // return JSC.ArrayBuffer.empty.toJS(global, null); + // return JSC.ArrayBuffer.create(global, "", .ArrayBuffer); // } // var bytes = this.InlineBlob.sliceConst(); // this.InlineBlob.len = 0; diff --git a/src/bun.js/webcore/body.zig b/src/bun.js/webcore/body.zig index ae873fa88..a4446446b 100644 --- a/src/bun.js/webcore/body.zig +++ b/src/bun.js/webcore/body.zig @@ -45,7 +45,7 @@ const StringJoiner = @import("../../string_joiner.zig"); const uws = @import("root").bun.uws; const Blob = JSC.WebCore.Blob; -const InlineBlob = JSC.WebCore.InlineBlob; +// const InlineBlob = JSC.WebCore.InlineBlob; const AnyBlob = JSC.WebCore.AnyBlob; const InternalBlob = JSC.WebCore.InternalBlob; const Response = JSC.WebCore.Response; @@ -100,6 +100,7 @@ pub const Body = struct { try formatter.writeIndent(Writer, writer); try this.value.Blob.writeFormat(Formatter, formatter, writer, enable_ansi_colors); } else if (this.value == .InternalBlob) { + // } else if (this.value == .InternalBlob or this.value == .InlineBlob) { try formatter.printComma(Writer, writer, enable_ansi_colors); try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); @@ -737,11 +738,12 @@ pub const Body = struct { }, // .InlineBlob => { // const cloned = this.InlineBlob.bytes; + // // keep same behavior as InternalBlob but clone the data // const new_blob = Blob.create( // cloned[0..this.InlineBlob.len], // bun.default_allocator, // JSC.VirtualMachine.get().global, - // this.InlineBlob.was_string, + // false, // ); // this.* = .{ .Used = {} }; @@ -855,7 +857,6 @@ pub const Body = struct { JSC.C.JSValueUnprotect(VirtualMachine.get().global, this.Error.asObjectRef()); } } - pub fn clone(this: *Value, globalThis: *JSC.JSGlobalObject) Value { if (this.* == .InternalBlob) { var internal_blob = this.InternalBlob; diff --git a/src/bun.js/webcore/request.zig b/src/bun.js/webcore/request.zig index 854f7ff43..a3df7caeb 100644 --- a/src/bun.js/webcore/request.zig +++ b/src/bun.js/webcore/request.zig @@ -52,6 +52,15 @@ const BodyMixin = JSC.WebCore.BodyMixin; const Body = JSC.WebCore.Body; const Blob = JSC.WebCore.Blob; +const body_value_pool_size: u16 = 256; +pub const BodyValueRef = bun.HiveRef(Body.Value, body_value_pool_size); +const BodyValueHiveAllocator = bun.HiveArray(BodyValueRef, body_value_pool_size).Fallback; + +var body_value_hive_allocator = BodyValueHiveAllocator.init(bun.default_allocator); + +pub fn InitRequestBodyValue(value: Body.Value) !*BodyValueRef { + return try BodyValueRef.init(value, &body_value_hive_allocator); +} // https://developer.mozilla.org/en-US/docs/Web/API/Request pub const Request = struct { url: []const u8 = "", @@ -59,7 +68,7 @@ pub const Request = struct { headers: ?*FetchHeaders = null, signal: ?*AbortSignal = null, - body: Body.Value = Body.Value{ .Null = {} }, + body: *BodyValueRef, method: Method = Method.GET, uws_request: ?*uws.Request = null, https: bool = false, @@ -94,9 +103,9 @@ pub const Request = struct { } } - if (this.body == .Blob) { - if (this.body.Blob.content_type.len > 0) - return ZigString.Slice.fromUTF8NeverFree(this.body.Blob.content_type); + if (this.body.value == .Blob) { + if (this.body.value.Blob.content_type.len > 0) + return ZigString.Slice.fromUTF8NeverFree(this.body.value.Blob.content_type); } return null; @@ -111,14 +120,14 @@ pub const Request = struct { pub fn estimatedSize(this: *Request) callconv(.C) usize { return this.reported_estimated_size orelse brk: { - this.reported_estimated_size = @truncate(u63, this.body.estimatedSize() + this.sizeOfURL() + @sizeOf(Request)); + this.reported_estimated_size = @truncate(u63, this.body.value.estimatedSize() + this.sizeOfURL() + @sizeOf(Request)); break :brk this.reported_estimated_size.?; }; } pub fn writeFormat(this: *Request, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { const Writer = @TypeOf(writer); - try writer.print("Request ({}) {{\n", .{bun.fmt.size(this.body.slice().len)}); + try writer.print("Request ({}) {{\n", .{bun.fmt.size(this.body.value.slice().len)}); { formatter.indent += 1; defer formatter.indent -|= 1; @@ -136,20 +145,20 @@ pub const Request = struct { try writer.print(comptime Output.prettyFmt("<r><b>{s}<r>", enable_ansi_colors), .{this.url}); try writer.writeAll("\""); - if (this.body == .Blob) { + if (this.body.value == .Blob) { try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); - try this.body.Blob.writeFormat(Formatter, formatter, writer, enable_ansi_colors); - } else if (this.body == .InternalBlob) { + try this.body.value.Blob.writeFormat(Formatter, formatter, writer, enable_ansi_colors); + } else if (this.body.value == .InternalBlob) { try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); - if (this.body.size() == 0) { + if (this.body.value.size() == 0) { try Blob.initEmpty(undefined).writeFormat(Formatter, formatter, writer, enable_ansi_colors); } else { - try Blob.writeFormatForSize(this.body.size(), writer, enable_ansi_colors); + try Blob.writeFormatForSize(this.body.value.size(), writer, enable_ansi_colors); } - } else if (this.body == .Locked) { - if (this.body.Locked.readable) |stream| { + } else if (this.body.value == .Locked) { + if (this.body.value.Locked.readable) |stream| { try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); formatter.printAs(.Object, Writer, writer, stream.value, stream.value.jsType(), enable_ansi_colors); @@ -164,7 +173,7 @@ pub const Request = struct { pub fn fromRequestContext(ctx: *RequestContext) !Request { var req = Request{ .url = bun.asByteSlice(ctx.getFullURL()), - .body = .{ .Null = {} }, + .body = try InitRequestBodyValue(.{ .Null = {} }), .method = ctx.method, .headers = FetchHeaders.createFromPicoHeaders(ctx.request.headers), .url_was_allocated = true, @@ -179,7 +188,7 @@ pub const Request = struct { } } - switch (this.body) { + switch (this.body.value) { .Blob => |blob| { if (blob.content_type.len > 0) { return blob.content_type; @@ -187,8 +196,8 @@ pub const Request = struct { return MimeType.other.value; }, - .InternalBlob => return this.body.InternalBlob.contentType(), - // .InlineBlob => return this.body.InlineBlob.contentType(), + .InternalBlob => return this.body.value.InternalBlob.contentType(), + // .InlineBlob => return this.body.value.InlineBlob.contentType(), .Null, .Error, .Used, .Locked, .Empty => return MimeType.other.value, } } @@ -270,7 +279,6 @@ pub const Request = struct { bun.default_allocator.free(bun.constStrToU8(this.url)); } - this.body.deinit(); if (this.signal) |signal| { _ = signal.unref(); this.signal = null; @@ -279,6 +287,7 @@ pub const Request = struct { pub fn finalize(this: *Request) callconv(.C) void { this.finalizeWithoutDeinit(); + _ = this.body.unref(); bun.default_allocator.destroy(this); } @@ -400,13 +409,19 @@ pub const Request = struct { globalThis: *JSC.JSGlobalObject, arguments: []const JSC.JSValue, ) ?Request { - var req = Request{}; + var req = Request{ + .body = InitRequestBodyValue(.{ .Null = {} }) catch { + return null; + }, + }; if (arguments.len == 0) { globalThis.throw("Failed to construct 'Request': 1 argument required, but only 0 present.", .{}); + _ = req.body.unref(); return null; } else if (arguments[0].isEmptyOrUndefinedOrNull() or !arguments[0].isCell()) { globalThis.throw("Failed to construct 'Request': expected non-empty string or object, got undefined", .{}); + _ = req.body.unref(); return null; } @@ -423,10 +438,12 @@ pub const Request = struct { if (is_first_argument_a_url) { const slice = arguments[0].toSliceOrNull(globalThis) orelse { req.finalizeWithoutDeinit(); + _ = req.body.unref(); return null; }; req.url = (slice.cloneIfNeeded(globalThis.allocator()) catch { req.finalizeWithoutDeinit(); + _ = req.body.unref(); return null; }).slice(); req.url_was_allocated = req.url.len > 0; @@ -434,6 +451,7 @@ pub const Request = struct { fields.insert(.url); } else if (!url_or_object_type.isObject()) { globalThis.throw("Failed to construct 'Request': expected non-empty string or object", .{}); + _ = req.body.unref(); return null; } @@ -476,10 +494,10 @@ pub const Request = struct { } if (!fields.contains(.body)) { - switch (request.body) { + switch (request.body.value) { .Null, .Empty, .Used => {}, else => { - req.body = request.body.clone(globalThis); + req.body.value = request.body.value.clone(globalThis); fields.insert(.body); }, } @@ -511,7 +529,7 @@ pub const Request = struct { switch (response.body.value) { .Null, .Empty, .Used => {}, else => { - req.body = response.body.value.clone(globalThis); + req.body.value = response.body.value.clone(globalThis); fields.insert(.body); }, } @@ -523,9 +541,10 @@ pub const Request = struct { if (value.fastGet(globalThis, .body)) |body_| { fields.insert(.body); if (Body.Value.fromJS(globalThis, body_)) |body| { - req.body = body; + req.body.value = body; } else { req.finalizeWithoutDeinit(); + _ = req.body.unref(); return null; } } @@ -546,10 +565,12 @@ pub const Request = struct { { const slice = value.toSliceOrNull(globalThis) orelse { req.finalizeWithoutDeinit(); + _ = req.body.unref(); return null; }; req.url = (slice.cloneIfNeeded(globalThis.allocator()) catch { req.finalizeWithoutDeinit(); + _ = req.body.unref(); return null; }).slice(); req.url_was_allocated = req.url.len > 0; @@ -569,6 +590,7 @@ pub const Request = struct { } else { globalThis.throw("Failed to construct 'Request': signal is not of type AbortSignal.", .{}); req.finalizeWithoutDeinit(); + _ = req.body.unref(); return null; } } @@ -596,6 +618,7 @@ pub const Request = struct { if (req.url.len == 0) { globalThis.throw("Failed to construct 'Request': url is required.", .{}); req.finalizeWithoutDeinit(); + _ = req.body.unref(); return null; } @@ -603,20 +626,20 @@ pub const Request = struct { if (parsed_url.hostname.len == 0) { globalThis.throw("Failed to construct 'Request': Invalid URL (missing a hostname)", .{}); req.finalizeWithoutDeinit(); + _ = req.body.unref(); return null; } - if (req.body == .Blob and + if (req.body.value == .Blob and req.headers != null and - req.body.Blob.content_type.len > 0 and + req.body.value.Blob.content_type.len > 0 and !req.headers.?.fastHas(.ContentType)) { - req.headers.?.put("content-type", req.body.Blob.content_type, globalThis); + req.headers.?.put("content-type", req.body.value.Blob.content_type, globalThis); } return req; } - pub fn constructor( globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, @@ -624,8 +647,12 @@ pub const Request = struct { const arguments_ = callframe.arguments(2); const arguments = arguments_.ptr[0..arguments_.len]; - const request = constructInto(globalThis, arguments) orelse return null; - var request_ = getAllocator(globalThis).create(Request) catch return null; + var request = constructInto(globalThis, arguments) orelse { + return null; + }; + var request_ = getAllocator(globalThis).create(Request) catch { + return null; + }; request_.* = request; return request_; } @@ -633,7 +660,7 @@ pub const Request = struct { pub fn getBodyValue( this: *Request, ) *Body.Value { - return &this.body; + return &this.body.value; } pub fn getFetchHeaders( @@ -661,8 +688,8 @@ pub const Request = struct { } else { this.headers = FetchHeaders.createEmpty(); - if (this.body == .Blob) { - const content_type = this.body.Blob.content_type; + if (this.body.value == .Blob) { + const content_type = this.body.value.Blob.content_type; if (content_type.len > 0) { this.headers.?.put("content-type", content_type, globalThis); } @@ -699,9 +726,15 @@ pub const Request = struct { ) void { this.ensureURL() catch {}; + var body = InitRequestBodyValue(this.body.value.clone(globalThis)) catch { + globalThis.throw("Failed to clone request", .{}); + return; + }; + req.* = Request{ - .body = this.body.clone(globalThis), + .body = body, .url = allocator.dupe(u8, this.url) catch { + _ = body.unref(); globalThis.throw("Failed to clone request", .{}); return; }, diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 7517395da..9c262252a 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -760,11 +760,6 @@ pub const Fetch = struct { fn toBodyValue(this: *FetchTasklet) Body.Value { var response_buffer = this.response_buffer.list; - const response = Body.Value{ - .InternalBlob = .{ - .bytes = response_buffer.toManaged(bun.default_allocator), - }, - }; this.response_buffer = .{ .allocator = default_allocator, .list = .{ @@ -778,6 +773,13 @@ pub const Fetch = struct { // defer response_buffer.deinit(bun.default_allocator); // return .{ .InlineBlob = inline_blob }; // } + + const response = Body.Value{ + .InternalBlob = .{ + .bytes = response_buffer.toManaged(bun.default_allocator), + }, + }; + return response; } @@ -1016,7 +1018,7 @@ pub const Fetch = struct { return JSC.JSValue.jsUndefined().asObjectRef(); } } else { - body = request.body.useAsAnyBlob(); + body = request.body.value.useAsAnyBlob(); } if (options.get(ctx, "timeout")) |timeout_value| { @@ -1095,7 +1097,7 @@ pub const Fetch = struct { } headers = Headers.from(head, bun.default_allocator) catch unreachable; } - body = request.body.useAsAnyBlob(); + body = request.body.value.useAsAnyBlob(); // no proxy only url url = ZigURL.parse(getAllocator(ctx).dupe(u8, request.url) catch unreachable); url_proxy_buffer = url.href; diff --git a/src/bun.zig b/src/bun.zig index d640b4a77..9fd6102e8 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -1361,4 +1361,72 @@ pub fn threadlocalAllocator() std.mem.Allocator { return default_allocator; } +pub fn Ref(comptime T: type) type { + return struct { + ref_count: u32, + allocator: std.mem.Allocator, + value: T, + + pub fn init(value: T, allocator: std.mem.Allocator) !*@This() { + var this = try allocator.create(@This()); + this.allocator = allocator; + this.ref_count = 1; + this.value = value; + return this; + } + + pub fn ref(this: *@This()) *@This() { + this.ref_count += 1; + return this; + } + + pub fn unref(this: *@This()) ?*@This() { + this.ref_count -= 1; + if (this.ref_count == 0) { + if (@hasDecl(T, "deinit")) { + this.value.deinit(); + } + this.allocator.destroy(this); + return null; + } + return this; + } + }; +} + +pub fn HiveRef(comptime T: type, comptime capacity: u16) type { + return struct { + const HiveAllocator = HiveArray(@This(), capacity).Fallback; + + ref_count: u32, + allocator: *HiveAllocator, + value: T, + + pub fn init(value: T, allocator: *HiveAllocator) !*@This() { + var this = try allocator.tryGet(); + this.allocator = allocator; + this.ref_count = 1; + this.value = value; + return this; + } + + pub fn ref(this: *@This()) *@This() { + this.ref_count += 1; + return this; + } + + pub fn unref(this: *@This()) ?*@This() { + this.ref_count -= 1; + if (this.ref_count == 0) { + if (@hasDecl(T, "deinit")) { + this.value.deinit(); + } + this.allocator.put(this); + return null; + } + return this; + } + }; +} + pub const MaxHeapAllocator = @import("./max_heap_allocator.zig").MaxHeapAllocator; diff --git a/src/hive_array.zig b/src/hive_array.zig index 39f10d324..cc032c44f 100644 --- a/src/hive_array.zig +++ b/src/hive_array.zig @@ -86,6 +86,14 @@ pub fn HiveArray(comptime T: type, comptime capacity: u16) type { return self.allocator.create(T) catch unreachable; } + pub fn tryGet(self: *This) !*T { + if (self.hive.get()) |value| { + return value; + } + + return try self.allocator.create(T); + } + pub fn put(self: *This, value: *T) void { if (self.hive.put(value)) return; diff --git a/src/js_ast.zig b/src/js_ast.zig index 3cf99211b..d2cb467d1 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -9326,7 +9326,7 @@ pub const Macro = struct { blob_ = resp.body.use(); } else if (value.as(JSC.WebCore.Request)) |resp| { mime_type = HTTP.MimeType.init(resp.mimeType()); - blob_ = resp.body.use(); + blob_ = resp.body.value.use(); } else if (value.as(JSC.WebCore.Blob)) |resp| { blob_ = resp.*; blob_.?.allocator = null; diff --git a/test/js/bun/http/app.jsx b/test/js/bun/http/app.jsx new file mode 100644 index 000000000..fa61492e7 --- /dev/null +++ b/test/js/bun/http/app.jsx @@ -0,0 +1,10 @@ +const App = () => ( + <html> + <body> + <h1>Hello World</h1> + <p>This is an example.</p> + </body> + </html> +); + +export default <App />; diff --git a/test/js/bun/http/bun-server.test.ts b/test/js/bun/http/bun-server.test.ts index 8c87ac422..3472dc498 100644 --- a/test/js/bun/http/bun-server.test.ts +++ b/test/js/bun/http/bun-server.test.ts @@ -60,11 +60,13 @@ describe("Server", () => { test("abort signal on server", async () => { { let signalOnServer = false; + const abortController = new AbortController(); const server = Bun.serve({ async fetch(req) { req.signal.addEventListener("abort", () => { signalOnServer = true; }); + abortController.abort(); await Bun.sleep(15); return new Response("Hello"); }, @@ -72,7 +74,7 @@ describe("Server", () => { }); try { - await fetch(`http://${server.hostname}:${server.port}`, { signal: AbortSignal.timeout(10) }); + await fetch(`http://${server.hostname}:${server.port}`, { signal: abortController.signal }); } catch {} expect(signalOnServer).toBe(true); server.stop(true); @@ -162,13 +164,10 @@ describe("Server", () => { return new Response( new ReadableStream({ async pull(controller) { - console.trace("here"); abortController.abort(); const buffer = await Bun.file(import.meta.dir + "/fixture.html.gz").arrayBuffer(); - console.trace("here"); controller.enqueue(buffer); - console.trace("here"); //wait to detect the connection abortion await Bun.sleep(15); @@ -188,15 +187,11 @@ describe("Server", () => { }); try { - console.trace("here"); await fetch(`http://${server.hostname}:${server.port}`, { signal: abortController.signal }); } catch {} await Bun.sleep(10); - console.trace("here"); expect(signalOnServer).toBe(true); - console.trace("here"); server.stop(true); - console.trace("here"); } }); }); diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index 46c4790e2..6deb80fb7 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -2,6 +2,8 @@ import { file, gc, Serve, serve, Server } from "bun"; import { afterEach, describe, it, expect, afterAll } from "bun:test"; import { readFileSync, writeFileSync } from "fs"; import { resolve } from "path"; +import { renderToReadableStream } from "react-dom/server"; +import app_jsx from "./app.jsx"; type Handler = (req: Request) => Response; afterEach(() => gc(true)); @@ -986,3 +988,31 @@ describe("should support Content-Range with Bun.file()", () => { }); } }); + +it("request body and signal life cycle", async () => { + { + const headers = { + headers: { + "Content-Type": "text/html", + }, + }; + + const server = Bun.serve({ + async fetch(req) { + await queueMicrotask(() => Bun.gc(true)); + return new Response(await renderToReadableStream(app_jsx), headers); + }, + }); + + try { + const requests = []; + for (let i = 0; i < 1000; i++) { + requests.push(fetch(`http://${server.hostname}:${server.port}`)); + } + await Promise.all(requests); + } catch {} + await Bun.sleep(10); + expect(true).toBe(true); + server.stop(true); + } +}); |