From a660692743ed7640bf32c27254be72ff8b417eaf Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 6 Oct 2024 06:12:23 -0700 Subject: [PATCH 01/36] Rewrite node:http --- packages/bun-uws/src/HttpResponse.h | 9 +- src/bun.js/api/server.classes.ts | 52 ++ src/bun.js/api/server.zig | 625 +++++++++++++- src/bun.js/bindings/ErrorCode.ts | 7 +- src/bun.js/bindings/NodeHTTP.cpp | 380 +++++++++ src/bun.js/bindings/bindings.cpp | 68 -- .../bindings/generated_classes_list.zig | 1 + src/bun.js/webcore/streams.zig | 2 +- src/bun.zig | 4 +- src/deps/libuwsockets.cpp | 8 +- src/deps/uws.zig | 47 +- src/js/builtins/ProcessObjectInternals.ts | 6 +- src/js/node/http.ts | 794 +++++++++++------- src/jsc.zig | 1 + 14 files changed, 1618 insertions(+), 386 deletions(-) diff --git a/packages/bun-uws/src/HttpResponse.h b/packages/bun-uws/src/HttpResponse.h index a58a86bd6ade5..1632266daee8c 100644 --- a/packages/bun-uws/src/HttpResponse.h +++ b/packages/bun-uws/src/HttpResponse.h @@ -455,11 +455,14 @@ struct HttpResponse : public AsyncSocket { } /* Write parts of the response in chunking fashion. Starts timeout if failed. */ - bool write(std::string_view data) { + bool write(std::string_view data, size_t *writtenPtr = nullptr) { writeStatus(HTTP_200_OK); /* Do not allow sending 0 chunks, they mark end of response */ if (!data.length()) { + if (writtenPtr) { + *writtenPtr = 0; + } /* If you called us, then according to you it was fine to call us so it's fine to still call us */ return true; } @@ -482,6 +485,10 @@ struct HttpResponse : public AsyncSocket { /* Reset timeout on each sended chunk */ this->resetTimeout(); + if (writtenPtr) { + *writtenPtr = written; + } + /* If we did not fail the write, accept more */ return !failed; } diff --git a/src/bun.js/api/server.classes.ts b/src/bun.js/api/server.classes.ts index 9182fa809e4f8..27868539d6a41 100644 --- a/src/bun.js/api/server.classes.ts +++ b/src/bun.js/api/server.classes.ts @@ -90,6 +90,58 @@ export default [ generate(`HTTPSServer`), generate(`DebugHTTPSServer`), + define({ + name: "NodeHTTPResponse", + JSType: "0b11101110", + proto: { + writeHead: { + fn: "writeHead", + length: 3, + }, + write: { + fn: "write", + length: 2, + }, + end: { + fn: "end", + length: 2, + }, + cork: { + fn: "cork", + length: 1, + }, + ref: { + fn: "jsRef", + }, + unref: { + fn: "jsUnref", + }, + abort: { + fn: "abort", + length: 0, + }, + ondata: { + getter: "getOnData", + setter: "setOnData", + }, + onabort: { + getter: "getOnAbort", + setter: "setOnAbort", + }, + // ontimeout: { + // getter: "getOnTimeout", + // setter: "setOnTimeout", + // }, + onwritable: { + getter: "getOnWritable", + setter: "setOnWritable", + }, + }, + klass: {}, + finalize: true, + noConstructor: true, + }), + define({ name: "ServerWebSocket", JSType: "0b11101110", diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index be5e09ad249e9..1078d33a2e601 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -454,6 +454,7 @@ pub const ServerConfig = struct { onError: JSC.JSValue = JSC.JSValue.zero, onRequest: JSC.JSValue = JSC.JSValue.zero, + onNodeHTTPRequest: JSC.JSValue = JSC.JSValue.zero, websocket: ?WebSocketServer = null, @@ -462,6 +463,8 @@ pub const ServerConfig = struct { id: []const u8 = "", allow_hot: bool = true, + is_node_http: bool = false, + static_routes: std.ArrayList(StaticRouteEntry) = std.ArrayList(StaticRouteEntry).init(bun.default_allocator), pub const StaticRouteEntry = struct { @@ -1433,6 +1436,16 @@ pub const ServerConfig = struct { } if (global.hasException()) return args; + if (arg.getTruthy(global, "onNodeHTTPRequest")) |onRequest_| { + if (!onRequest_.isCallable(global.vm())) { + JSC.throwInvalidArguments("Expected onNodeHTTPRequest to be a function", .{}, global, exception); + return args; + } + const onRequest = onRequest_.withAsyncContextIfNeeded(global); + JSC.C.JSValueProtect(global, onRequest.asObjectRef()); + args.onNodeHTTPRequest = onRequest; + } + if (arg.getTruthy(global, "fetch")) |onRequest_| { if (!onRequest_.isCallable(global.vm())) { JSC.throwInvalidArguments("Expected fetch() to be a function", .{}, global, exception); @@ -3547,7 +3560,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp // we can't do buffering ourselves here or it won't work // uSockets will append and manage the buffer // so any write will buffer if the write fails - if (resp.write(chunk)) { + if (resp.write(chunk) == .want_more) { if (stream.isDone()) { this.endStream(this.shouldCloseConnection()); } @@ -5783,6 +5796,534 @@ pub const ServerWebSocket = struct { } }; +pub const NodeHTTPResponse = struct { + response: uws.AnyResponse, + onDataCallback: JSC.Strong = .{}, + onWritableCallback: JSC.Strong = .{}, + onAbortedCallback: JSC.Strong = .{}, + strong_this: JSC.Strong = .{}, + + ref_count: u32 = 1, + js_ref: JSC.Ref = .{}, + aborted: bool = false, + finished: bool = false, + ended: bool = false, + has_body: bool = false, + + const log = bun.Output.scoped(.NodeHTTPResponse, false); + + pub usingnamespace JSC.Codegen.JSNodeHTTPResponse; + pub usingnamespace bun.NewRefCounted(@This(), deinit); + pub fn create( + globalObject: *JSC.JSGlobalObject, + has_body: *i32, + request: *uws.Request, + is_ssl: i32, + response_ptr: *anyopaque, + node_response_ptr: *?*NodeHTTPResponse, + ) callconv(.C) JSC.JSValue { + if ((HTTP.Method.which(request.method()) orelse HTTP.Method.OPTIONS).hasRequestBody()) { + const req_len: usize = brk: { + if (request.header("content-length")) |content_length| { + break :brk std.fmt.parseInt(usize, content_length, 10) catch 0; + } + + break :brk 0; + }; + + has_body.* = @intFromBool(req_len > 0 or request.header("transfer-encoding") != null); + } + + const response = NodeHTTPResponse.new(.{ + .response = switch (is_ssl != 0) { + true => uws.AnyResponse{ .SSL = @ptrCast(response_ptr) }, + false => uws.AnyResponse{ .TCP = @ptrCast(response_ptr) }, + }, + .has_body = has_body.* != 0, + // 1 - the HTTP request + // 1 - the JS object + // 1 - the Server handler. + .ref_count = 3, + }); + response.js_ref.ref(globalObject.bunVM()); + const js_this = response.toJS(globalObject); + response.strong_this.set(globalObject, js_this); + node_response_ptr.* = response; + return js_this; + } + + pub fn setOnAbortedHandler(this: *NodeHTTPResponse) void { + this.response.onAborted(*NodeHTTPResponse, onAbort, this); + } + + fn isDone(this: *const NodeHTTPResponse) bool { + return this.finished or this.ended or this.aborted; + } + + pub fn jsRef(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) JSC.JSValue { + if (!this.isDone()) { + this.js_ref.ref(globalObject.bunVM()); + } + return .undefined; + } + + pub fn jsUnref(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) JSC.JSValue { + if (!this.isDone()) { + this.js_ref.unref(globalObject.bunVM()); + } + return .undefined; + } + + fn handleEndedIfNecessary(state: uws.State, globalObject: *JSC.JSGlobalObject) bool { + if (state.isHttpEndCalled()) { + globalObject.ERR_HTTP_HEADERS_SENT("Stream is already ended", .{}).throw(); + return true; + } + return false; + } + + extern "C" fn NodeHTTPServer__writeHead_http( + globalObject: *JSC.JSGlobalObject, + statusMessage: [*]const u8, + statusMessageLength: usize, + headersObjectValue: JSC.JSValue, + response: *anyopaque, + ) void; + + extern "C" fn NodeHTTPServer__writeHead_https( + globalObject: *JSC.JSGlobalObject, + statusMessage: [*]const u8, + statusMessageLength: usize, + headersObjectValue: JSC.JSValue, + response: *anyopaque, + ) void; + + pub fn writeHead(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + const arguments = callframe.argumentsUndef(3).slice(); + + if (this.isDone()) { + globalObject.ERR_STREAM_ALREADY_FINISHED("Stream is already ended", .{}).throw(); + return .undefined; + } + + const state = this.response.state(); + if (handleEndedIfNecessary(state, globalObject)) { + return .zero; + } + if (state.isHttpWriteCalled() or state.isHttpStatusCalled()) { + globalObject.ERR_HTTP_HEADERS_SENT("Stream already started", .{}).throw(); + return .zero; + } + + const status_code_value = if (arguments.len > 0) arguments[0] else .undefined; + const status_message_value = if (arguments.len > 1) arguments[1] else .undefined; + const headers_object_value = if (arguments.len > 2) arguments[2] else .undefined; + + const status_code: i32 = brk: { + if (status_code_value != .undefined) { + break :brk globalObject.validateIntegerRange(status_code_value, i32, 200, .{ + .min = 100, + .max = 599, + }) orelse return .zero; + } + + break :brk 200; + }; + + const status_message_slice = if (status_message_value != .undefined) + status_message_value.toSlice(globalObject, bun.default_allocator) + else + ZigString.Slice.empty; + defer status_message_slice.deinit(); + if (globalObject.hasException()) { + return .zero; + } + + do_it: { + if (status_message_slice.len == 0) { + if (HTTPStatusText.get(@intCast(status_code))) |status_message| { + writeHeadInternal(this.response, globalObject, status_message, headers_object_value); + break :do_it; + } + } + + const message = if (status_message_slice.len > 0) status_message_slice.slice() else "HM"; + const status_message = std.fmt.allocPrint(bun.default_allocator, "{d} {s}", .{ status_code, message }) catch bun.outOfMemory(); + defer bun.default_allocator.free(status_message); + writeHeadInternal(this.response, globalObject, status_message, headers_object_value); + break :do_it; + } + + return .undefined; + } + + fn writeHeadInternal(response: uws.AnyResponse, globalObject: *JSC.JSGlobalObject, status_message: []const u8, headers: JSC.JSValue) void { + log("writeHeadInternal({s})", .{status_message}); + switch (response) { + .TCP => NodeHTTPServer__writeHead_http(globalObject, status_message.ptr, status_message.len, headers, @ptrCast(response.TCP)), + .SSL => NodeHTTPServer__writeHead_https(globalObject, status_message.ptr, status_message.len, headers, @ptrCast(response.SSL)), + } + } + + pub fn writeContinue(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + const arguments = callframe.arguments(1).slice(); + _ = arguments; // autofix + if (this.isDone()) { + return .undefined; + } + + const state = this.response.state(); + if (handleEndedIfNecessary(state, globalObject)) { + return .zero; + } + + this.response.writeContinue(); + return .undefined; + } + + pub fn onAbort(this: *NodeHTTPResponse, resp: uws.AnyResponse) void { + _ = resp; // autofix + if (this.finished) { + return; + } + defer this.onRequestComplete(); + this.aborted = true; + log("onAbort", .{}); + + this.ref(); + defer this.deref(); + + const js_this = this.strong_this.trySwap() orelse .undefined; + + if (this.onAbortedCallback.trySwap()) |on_aborted| { + const globalThis = this.onAbortedCallback.globalThis orelse JSC.VirtualMachine.get().global; + const vm = globalThis.bunVM(); + const event_loop = vm.eventLoop(); + + event_loop.runCallback(on_aborted, globalThis, js_this, &.{ + js_this, + }); + } + + if (this.onDataCallback.trySwap()) |onDataCallback| { + defer this.deref(); + const globalThis = this.onDataCallback.globalThis orelse JSC.VirtualMachine.get().global; + const vm = globalThis.bunVM(); + const event_loop = vm.eventLoop(); + event_loop.runCallback(onDataCallback, globalThis, .undefined, &.{ .undefined, JSC.jsBoolean(true), JSC.jsBoolean(true) }); + } + } + + fn onRequestComplete(this: *NodeHTTPResponse) void { + if (this.finished) { + return; + } + log("onRequestComplete", .{}); + this.finished = true; + this.js_ref.unref(JSC.VirtualMachine.get()); + this.clearJSValues(); + this.deref(); + } + + fn clearJSValues(this: *NodeHTTPResponse) void { + this.onDataCallback.deinit(); + this.strong_this.deinit(); + this.onWritableCallback.deinit(); + this.onAbortedCallback.deinit(); + } + + pub fn abort(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + _ = globalObject; // autofix + _ = callframe; // autofix + if (this.isDone()) { + return .undefined; + } + + this.aborted = true; + const state = this.response.state(); + if (state.isHttpEndCalled()) { + return .undefined; + } + + this.response.clearAborted(); + this.response.clearOnData(); + this.response.clearOnWritable(); + this.response.clearTimeout(); + this.response.endWithoutBody(true); + this.onRequestComplete(); + return .undefined; + } + + pub fn onData(this: *NodeHTTPResponse, chunk: []const u8, last: bool) void { + log("onData({d} bytes, is_last = {d})", .{ chunk.len, @intFromBool(last) }); + if (this.onDataCallback.get()) |callback| { + if (last) this.ref(); + defer { + if (last) { + this.onDataCallback.deinit(); + this.deref(); + } + } + const globalThis = this.onDataCallback.globalThis orelse JSC.VirtualMachine.get().global; + const event_loop = globalThis.bunVM().eventLoop(); + event_loop.runCallback(callback, globalThis, .undefined, &.{ + JSC.ArrayBuffer.createBuffer(globalThis, chunk), + JSC.JSValue.jsBoolean(last), + JSC.JSValue.jsBoolean(false), + }); + } else { + this.response.clearOnData(); + this.onDataCallback.deinit(); + this.deref(); + } + } + + fn onDrain(this: *NodeHTTPResponse, offset: u64, response: uws.AnyResponse) bool { + log("onDrain({d})", .{offset}); + this.ref(); + defer this.deref(); + response.clearOnWritable(); + if (this.aborted or this.finished) { + return false; + } + const on_writable = this.onWritableCallback.trySwap() orelse return false; + const globalThis = this.onWritableCallback.globalThis orelse JSC.VirtualMachine.get().global; + const vm = globalThis.bunVM(); + + response.corked(JSC.EventLoop.runCallback, .{ vm.eventLoop(), on_writable, globalThis, .undefined, &.{JSC.JSValue.jsNumberFromUint64(offset)} }); + if (this.aborted or this.finished) { + return false; + } + + return true; + } + + fn writeOrEnd( + this: *NodeHTTPResponse, + globalObject: *JSC.JSGlobalObject, + arguments: []const JSC.JSValue, + comptime is_end: bool, + ) JSC.JSValue { + if (this.isDone()) { + globalObject.ERR_STREAM_WRITE_AFTER_END("Stream already ended", .{}).throw(); + return .zero; + } + + const state = this.response.state(); + if (state.isHttpEndCalled()) { + globalObject.ERR_STREAM_WRITE_AFTER_END("Stream already ended", .{}).throw(); + return .zero; + } + + const input_value = if (arguments.len > 0) arguments[0] else .undefined; + var encoding_value = if (arguments.len > 1) arguments[1] else .undefined; + const callback_value = brk: { + if (encoding_value != .undefined and encoding_value.isCallable(globalObject.vm())) { + encoding_value = .undefined; + break :brk arguments[1]; + } + + if (arguments.len > 2 and arguments[2] != .undefined) { + if (!arguments[2].isCallable(globalObject.vm())) { + return globalObject.throwInvalidArgumentTypeValue("callback", "function", arguments[2]); + } + + break :brk arguments[2]; + } + + break :brk .undefined; + }; + + const string_or_buffer: JSC.Node.StringOrBuffer = brk: { + if (input_value == .null) { + globalObject.ERR_STREAM_NULL_VALUES("Cannot write null value to stream", .{}).throw(); + return .zero; + } + + var encoding: JSC.Node.Encoding = .utf8; + if (encoding_value != .undefined and encoding_value != .null) { + if (!encoding_value.isString()) { + return globalObject.throwInvalidArgumentTypeValue("encoding", "string", encoding_value); + } + + encoding = JSC.Node.Encoding.fromJS(encoding_value, globalObject) orelse { + globalObject.throwInvalidArguments("Invalid encoding", .{}); + return .zero; + }; + } + + break :brk JSC.Node.StringOrBuffer.fromJSWithEncoding(globalObject, bun.default_allocator, input_value, encoding) orelse { + if (!globalObject.hasException()) { + return globalObject.throwInvalidArgumentTypeValue("input", "string or buffer", input_value); + } + return .zero; + }; + }; + defer string_or_buffer.deinit(); + + if (globalObject.hasException()) { + return .zero; + } + + const bytes = string_or_buffer.slice(); + + const write_offset = this.response.getWriteOffset(); + + if (is_end) { + this.clearOnDataCallback(); + this.response.clearOnWritable(); + this.response.clearAborted(); + this.response.clearTimeout(); + } + + if (is_end and bytes.len == 0 and write_offset == 0) { + this.response.endWithoutBody(state.isHttpConnectionClose()); + this.onRequestComplete(); + return JSC.JSValue.jsNumberFromInt32(0); + } + + if (is_end) { + this.response.end(bytes, state.isHttpConnectionClose()); + this.onRequestComplete(); + return JSC.JSValue.jsNumberFromUint64(bytes.len); + } else { + switch (this.response.write(bytes)) { + .want_more => |written| { + this.response.clearOnWritable(); + this.onWritableCallback.clear(); + return JSC.JSValue.jsNumberFromUint64(written); + }, + .backpressure => |written| { + if (callback_value != .undefined) { + this.onWritableCallback.set(globalObject, callback_value.withAsyncContextIfNeeded(globalObject)); + this.response.onWritable(*NodeHTTPResponse, onDrain, this); + } + return JSC.JSValue.jsNumberFromInt64(-@as(i64, @intCast(written))); + }, + } + } + } + + pub fn setOnWritable(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, value: JSValue) bool { + if (this.isDone() or value == .undefined) { + this.onWritableCallback.clear(); + return true; + } + + this.onWritableCallback.set(globalObject, value.withAsyncContextIfNeeded(globalObject)); + return true; + } + + pub fn getOnWritable(this: *NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + return this.onWritableCallback.get() orelse .undefined; + } + + pub fn getOnAbort(this: *NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + return this.onAbortedCallback.get() orelse .undefined; + } + + pub fn setOnAbort(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, value: JSValue) bool { + if (this.isDone() or value == .undefined) { + this.onAbortedCallback.clear(); + return true; + } + + this.onAbortedCallback.set(globalObject, value.withAsyncContextIfNeeded(globalObject)); + return true; + } + + pub fn getOnData(this: *NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + return this.onDataCallback.get() orelse .undefined; + } + + fn clearOnDataCallback(this: *NodeHTTPResponse) void { + if (this.onDataCallback.has()) { + this.onDataCallback.deinit(); + this.response.clearOnData(); + this.deref(); + } + } + + pub fn setOnData(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, value: JSValue) bool { + if (!this.has_body or this.isDone() or value == .undefined) { + this.clearOnDataCallback(); + return true; + } + + if (!this.onDataCallback.has()) { + this.ref(); + } + this.onDataCallback.set(globalObject, value.withAsyncContextIfNeeded(globalObject)); + this.response.onData(*NodeHTTPResponse, onData, this); + return true; + } + + pub fn write(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + const arguments = callframe.arguments(3).slice(); + + return writeOrEnd(this, globalObject, arguments, false); + } + + pub fn end(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + const arguments = callframe.arguments(3).slice(); + return writeOrEnd(this, globalObject, arguments, true); + } + + fn handleCorked(globalObject: *JSC.JSGlobalObject, function: JSC.JSValue, result: *JSValue, is_exception: *bool) void { + result.* = function.call(globalObject, .undefined, &.{}) catch |err| { + result.* = globalObject.takeException(err); + is_exception.* = true; + return; + }; + } + pub fn cork(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + const arguments = callframe.arguments(1).slice(); + if (arguments.len == 0 or !arguments[0].isCallable(globalObject.vm())) { + return globalObject.throwInvalidArgumentTypeValue("cork", "function", arguments[0]); + } + + var result: JSC.JSValue = .zero; + var is_exception: bool = false; + this.ref(); + defer this.deref(); + + this.response.corked(handleCorked, .{ globalObject, arguments[0], &result, &is_exception }); + + if (is_exception) { + if (result != .zero) { + globalObject.throwValue(result); + } else { + globalObject.throw("unknown error", .{}); + } + return .zero; + } + + if (result == .zero) { + return .undefined; + } + + return result; + } + pub fn finalize(this: *NodeHTTPResponse) void { + this.clearJSValues(); + this.deref(); + } + + pub fn deinit(this: *NodeHTTPResponse) void { + this.js_ref.unref(JSC.VirtualMachine.get()); + this.onAbortedCallback.deinit(); + this.onDataCallback.deinit(); + this.onWritableCallback.deinit(); + this.strong_this.deinit(); + + this.destroy(); + } + + comptime { + @export(create, .{ .name = "NodeHTTPResponse__createForJS" }); + } +}; + pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { return struct { pub const ssl_enabled = ssl_enabled_; @@ -6126,6 +6667,10 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp this.config.onRequest.unprotect(); this.config.onRequest = new_config.onRequest; } + if (this.config.onNodeHTTPRequest != new_config.onNodeHTTPRequest) { + this.config.onNodeHTTPRequest.unprotect(); + this.config.onNodeHTTPRequest = new_config.onNodeHTTPRequest; + } if (this.config.onError != new_config.onError) { this.config.onError.unprotect(); this.config.onError = new_config.onError; @@ -6849,6 +7394,59 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp this.pending_requests += 1; } + pub fn onNodeHTTPRequest( + this: *ThisServer, + req: *uws.Request, + resp: *App.Response, + ) void { + JSC.markBinding(@src()); + this.onPendingRequest(); + if (comptime Environment.isDebug) { + this.vm.eventLoop().debug.enter(); + } + defer { + if (comptime Environment.isDebug) { + this.vm.eventLoop().debug.exit(); + } + } + req.setYield(false); + resp.timeout(this.config.idleTimeout); + + var node_http_response: ?*NodeHTTPResponse = null; + defer { + if (node_http_response) |node_response| { + node_response.deref(); + } + } + + const result = onNodeHTTPRequestFn( + this.globalThis, + this.thisObject, + this.config.onNodeHTTPRequest, + req, + resp, + &node_http_response, + ); + + if (result.toError()) |err| { + _ = this.vm.uncaughtException(this.globalThis, err, false); + return; + } + + this.vm.drainMicrotasks(); + + if (node_http_response) |node_response| { + if (!node_response.isDone()) { + node_response.setOnAbortedHandler(); + } + } + } + + const onNodeHTTPRequestFn = if (ssl_enabled) + NodeHTTPServer__onRequest_https + else + NodeHTTPServer__onRequest_http; + pub fn onRequest( this: *ThisServer, req: *uws.Request, @@ -7067,7 +7665,11 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp ); } - this.app.any("/*", *ThisServer, this, onRequest); + if (this.config.onNodeHTTPRequest != .zero) { + this.app.any("/*", *ThisServer, this, onNodeHTTPRequest); + } else if (this.config.onRequest != .zero) { + this.app.any("/*", *ThisServer, this, onRequest); + } if (comptime debug_mode) { this.app.get("/bun:info", *ThisServer, this, onBunInfoRequest); @@ -7267,5 +7869,24 @@ pub export fn Server__setIdleTimeout( comptime { if (!JSC.is_bindgen) { _ = Server__setIdleTimeout; + _ = NodeHTTPResponse.create; } } + +extern fn NodeHTTPServer__onRequest_http( + globalThis: *JSC.JSGlobalObject, + this: JSC.JSValue, + callback: JSC.JSValue, + request: *uws.Request, + response: *uws.NewApp(false).Response, + node_response_ptr: *?*NodeHTTPResponse, +) JSC.JSValue; + +extern fn NodeHTTPServer__onRequest_https( + globalThis: *JSC.JSGlobalObject, + this: JSC.JSValue, + callback: JSC.JSValue, + request: *uws.Request, + response: *uws.NewApp(true).Response, + node_response_ptr: *?*NodeHTTPResponse, +) JSC.JSValue; diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index 53b1796144a92..52d3e081507c9 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -48,7 +48,12 @@ export default [ ["ERR_BUFFER_OUT_OF_BOUNDS", RangeError, "RangeError"], ["ERR_UNKNOWN_SIGNAL", TypeError, "TypeError"], ["ERR_SOCKET_BAD_PORT", RangeError, "RangeError"], - + ["ERR_HTTP_HEADERS_SENT", Error, "Error"], + ["ERR_HTTP_BODY_NOT_ALLOWED", Error, "Error"], + ["ERR_HTTP_INVALID_STATUS_CODE", RangeError, "RangeError"], + ["ERR_HTTP_INVALID_HEADER_VALUE", TypeError, "TypeError"], + ["ERR_INVALID_CHAR", TypeError, "TypeError"], + ["ERR_METHOD_NOT_IMPLEMENTED", Error, "Error"], // Bun-specific ["ERR_FORMDATA_PARSE_ERROR", TypeError, "TypeError"], ["ERR_BODY_ALREADY_USED", Error, "Error"], diff --git a/src/bun.js/bindings/NodeHTTP.cpp b/src/bun.js/bindings/NodeHTTP.cpp index 2ff078e67d62e..e0d219827e64d 100644 --- a/src/bun.js/bindings/NodeHTTP.cpp +++ b/src/bun.js/bindings/NodeHTTP.cpp @@ -94,6 +94,164 @@ static EncodedJSValue assignHeadersFromFetchHeaders(FetchHeaders& impl, JSObject return JSValue::encode(tuple); } +static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, MarkedArgumentBuffer& args, JSC::JSGlobalObject* globalObject, JSC::VM& vm) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + std::string_view fullURLStdStr = request->getFullUrl(); + String fullURL = String::fromUTF8ReplacingInvalidSequences({ reinterpret_cast(fullURLStdStr.data()), fullURLStdStr.length() }); + + // Get the URL. + { + args.append(jsString(vm, fullURL)); + } + + // Get the method. + { + std::string_view methodView = request->getMethod(); + WTF::String methodString; + switch (methodView.length()) { + case 3: { + if (methodView == std::string_view("get", 3)) { + methodString = "GET"_s; + break; + } + if (methodView == std::string_view("put", 3)) { + methodString = "PUT"_s; + break; + } + + break; + } + case 4: { + if (methodView == std::string_view("post", 4)) { + methodString = "POST"_s; + break; + } + if (methodView == std::string_view("head", 4)) { + methodString = "HEAD"_s; + break; + } + + if (methodView == std::string_view("copy", 4)) { + methodString = "COPY"_s; + break; + } + } + + case 5: { + if (methodView == std::string_view("patch", 5)) { + methodString = "PATCH"_s; + break; + } + if (methodView == std::string_view("merge", 5)) { + methodString = "MERGE"_s; + break; + } + if (methodView == std::string_view("trace", 5)) { + methodString = "TRACE"_s; + break; + } + if (methodView == std::string_view("fetch", 5)) { + methodString = "FETCH"_s; + break; + } + if (methodView == std::string_view("purge", 5)) { + methodString = "PURGE"_s; + break; + } + + break; + } + + case 6: { + if (methodView == std::string_view("delete", 6)) { + methodString = "DELETE"_s; + break; + } + + break; + } + + case 7: { + if (methodView == std::string_view("connect", 7)) { + methodString = "CONNECT"_s; + break; + } + if (methodView == std::string_view("options", 7)) { + methodString = "OPTIONS"_s; + break; + } + + break; + } + } + + if (methodString.isNull()) { + methodString = String::fromUTF8ReplacingInvalidSequences({ reinterpret_cast(methodView.data()), methodView.length() }); + } + + args.append(jsString(vm, methodString)); + } + + size_t size = 0; + for (auto it = request->begin(); it != request->end(); ++it) { + size++; + } + + JSC::JSObject* headersObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), std::min(size, static_cast(JSFinalObject::maxInlineCapacity))); + RETURN_IF_EXCEPTION(scope, void()); + JSC::JSArray* array = constructEmptyArray(globalObject, nullptr, size * 2); + JSC::JSArray* setCookiesHeaderArray = nullptr; + JSC::JSString* setCookiesHeaderString = nullptr; + + args.append(headersObject); + args.append(array); + + unsigned i = 0; + + for (auto it = request->begin(); it != request->end(); ++it) { + auto pair = *it; + StringView nameView = StringView(std::span { reinterpret_cast(pair.first.data()), pair.first.length() }); + LChar* data = nullptr; + auto value = String::createUninitialized(pair.second.length(), data); + if (pair.second.length() > 0) + memcpy(data, pair.second.data(), pair.second.length()); + + HTTPHeaderName name; + WTF::String nameString; + WTF::String lowercasedNameString; + + if (WebCore::findHTTPHeaderName(nameView, name)) { + nameString = WTF::httpHeaderNameStringImpl(name); + lowercasedNameString = nameString; + } else { + nameString = nameView.toString(); + lowercasedNameString = nameString.convertToASCIILowercase(); + } + + JSString* jsValue = jsString(vm, value); + + if (name == WebCore::HTTPHeaderName::SetCookie) { + if (!setCookiesHeaderArray) { + setCookiesHeaderArray = constructEmptyArray(globalObject, nullptr); + setCookiesHeaderString = jsString(vm, nameString); + headersObject->putDirect(vm, Identifier::fromString(vm, lowercasedNameString), setCookiesHeaderArray, 0); + RETURN_IF_EXCEPTION(scope, void()); + } + array->putDirectIndex(globalObject, i++, setCookiesHeaderString); + array->putDirectIndex(globalObject, i++, jsValue); + setCookiesHeaderArray->push(globalObject, jsValue); + RETURN_IF_EXCEPTION(scope, void()); + + } else { + headersObject->putDirect(vm, Identifier::fromString(vm, lowercasedNameString), jsValue, 0); + array->putDirectIndex(globalObject, i++, jsString(vm, nameString)); + array->putDirectIndex(globalObject, i++, jsValue); + RETURN_IF_EXCEPTION(scope, void()); + } + } +} + // This is an 8% speedup. static EncodedJSValue assignHeadersFromUWebSockets(uWS::HttpRequest* request, JSObject* prototype, JSObject* objectValue, JSC::InternalFieldTuple* tuple, JSC::JSGlobalObject* globalObject, JSC::VM& vm) { @@ -257,6 +415,219 @@ static EncodedJSValue assignHeadersFromUWebSockets(uWS::HttpRequest* request, JS return JSValue::encode(tuple); } +extern "C" EncodedJSValue NodeHTTPResponse__createForJS(JSC::JSGlobalObject* globalObject, int* hasBody, uWS::HttpRequest* request, int isSSL, void* response_ptr, void** nodeHttpResponsePtr); + +template +static EncodedJSValue NodeHTTPServer__onRequest( + JSC::JSGlobalObject* globalObject, + JSValue thisValue, + JSValue callback, + uWS::HttpRequest* request, + uWS::HttpResponse* response, + void** nodeHttpResponsePtr) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSObject* callbackObject = jsCast(callback); + MarkedArgumentBuffer args; + args.append(thisValue); + + assignHeadersFromUWebSocketsForCall(request, args, globalObject, vm); + if (scope.exception()) { + auto* exception = scope.exception(); + response->endWithoutBody(); + scope.clearException(); + return JSValue::encode(exception); + } + + int hasBody = 0; + EncodedJSValue nodehttpobjectValue = NodeHTTPResponse__createForJS(globalObject, &hasBody, request, isSSL, response, nodeHttpResponsePtr); + + JSC::CallData callData = getCallData(callbackObject); + args.append(JSValue::decode(nodehttpobjectValue)); + args.append(jsBoolean(hasBody)); + + WTF::NakedPtr exception; + JSValue returnValue = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, jsUndefined(), args, exception); + if (exception) { + auto* ptr = exception.get(); + exception.clear(); + return JSValue::encode(ptr); + } + + return JSValue::encode(returnValue); +} + +template +static void writeResponseHeader(uWS::HttpResponse* res, const WTF::StringView& name, const WTF::StringView& value) +{ + WTF::CString nameStr; + WTF::CString valueStr; + + std::string_view nameView; + std::string_view valueView; + + if (name.is8Bit()) { + const auto nameSpan = name.span8(); + ASSERT(name.containsOnlyASCII()); + nameView = std::string_view(reinterpret_cast(nameSpan.data()), nameSpan.size()); + } else { + nameStr = name.utf8(); + nameView = std::string_view(nameStr.data(), nameStr.length()); + } + + if (value.is8Bit()) { + const auto valueSpan = value.span8(); + valueView = std::string_view(reinterpret_cast(valueSpan.data()), valueSpan.size()); + } else { + valueStr = value.utf8(); + valueView = std::string_view(valueStr.data(), valueStr.length()); + } + + res->writeHeader(nameView, valueView); +} + +template +static void writeFetchHeadersToUWSResponse(WebCore::FetchHeaders& headers, uWS::HttpResponse* res) +{ + auto& internalHeaders = headers.internalHeaders(); + + for (auto& value : internalHeaders.getSetCookieHeaders()) { + + if (value.is8Bit()) { + const auto valueSpan = value.span8(); + res->writeHeader(std::string_view("set-cookie", 10), std::string_view(reinterpret_cast(valueSpan.data()), valueSpan.size())); + } else { + WTF::CString valueStr = value.utf8(); + res->writeHeader(std::string_view("set-cookie", 10), std::string_view(valueStr.data(), valueStr.length())); + } + } + + for (const auto& header : internalHeaders.commonHeaders()) { + const auto& name = WebCore::httpHeaderNameString(header.key); + const auto& value = header.value; + + writeResponseHeader(res, name, value); + } + + for (auto& header : internalHeaders.uncommonHeaders()) { + const auto& name = header.key; + const auto& value = header.value; + + writeResponseHeader(res, name, value); + } +} + +template +static void NodeHTTPServer__writeHead( + JSC::JSGlobalObject* globalObject, + const char* statusMessage, + size_t statusMessageLength, + JSValue headersObjectValue, + uWS::HttpResponse* response) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSObject* headersObject = headersObjectValue.getObject(); + response->writeStatus(std::string_view(statusMessage, statusMessageLength)); + + if (headersObject) { + if (auto* fetchHeaders = jsDynamicCast(headersObject)) { + writeFetchHeadersToUWSResponse(fetchHeaders->wrapped(), response); + return; + } + + if (UNLIKELY(headersObject->hasNonReifiedStaticProperties())) { + headersObject->reifyAllStaticProperties(globalObject); + RETURN_IF_EXCEPTION(scope, void()); + } + + auto* structure = headersObject->structure(); + + if (structure->canPerformFastPropertyEnumeration()) { + structure->forEachProperty(vm, [&](const auto& entry) { + JSValue headerValue = headersObject->getDirect(entry.offset()); + if (!headerValue.isString()) { + + return true; + } + + String key = entry.key(); + String value = headerValue.toWTFString(globalObject); + if (scope.exception()) { + return false; + } + + writeResponseHeader(response, key, value); + + return true; + }); + } else { + PropertyNameArray propertyNames(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude); + headersObject->getOwnPropertyNames(headersObject, globalObject, propertyNames, DontEnumPropertiesMode::Exclude); + RETURN_IF_EXCEPTION(scope, void()); + + for (unsigned i = 0; i < propertyNames.size(); ++i) { + JSValue headerValue = headersObject->getIfPropertyExists(globalObject, propertyNames[i]); + if (!headerValue.isString()) { + continue; + } + + String key = propertyNames[i].string(); + String value = headerValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, void()); + writeResponseHeader(response, key, value); + } + } + } + + RELEASE_AND_RETURN(scope, void()); +} + +extern "C" void NodeHTTPServer__writeHead_http( + JSC::JSGlobalObject* globalObject, + const char* statusMessage, + size_t statusMessageLength, + JSValue headersObjectValue, + uWS::HttpResponse* response) +{ + return NodeHTTPServer__writeHead(globalObject, statusMessage, statusMessageLength, headersObjectValue, response); +} + +extern "C" void NodeHTTPServer__writeHead_https( + JSC::JSGlobalObject* globalObject, + const char* statusMessage, + size_t statusMessageLength, + JSValue headersObjectValue, + uWS::HttpResponse* response) +{ + return NodeHTTPServer__writeHead(globalObject, statusMessage, statusMessageLength, headersObjectValue, response); +} + +extern "C" EncodedJSValue NodeHTTPServer__onRequest_http( + JSC::JSGlobalObject* globalObject, + EncodedJSValue thisValue, + EncodedJSValue callback, + uWS::HttpRequest* request, + uWS::HttpResponse* response, + void** nodeHttpResponsePtr) +{ + return NodeHTTPServer__onRequest(globalObject, JSValue::decode(thisValue), JSValue::decode(callback), request, response, nodeHttpResponsePtr); +} + +extern "C" EncodedJSValue NodeHTTPServer__onRequest_https( + JSC::JSGlobalObject* globalObject, + EncodedJSValue thisValue, + EncodedJSValue callback, + uWS::HttpRequest* request, + uWS::HttpResponse* response, + void** nodeHttpResponsePtr) +{ + return NodeHTTPServer__onRequest(globalObject, JSValue::decode(thisValue), JSValue::decode(callback), request, response, nodeHttpResponsePtr); +} + JSC_DEFINE_HOST_FUNCTION(jsHTTPAssignHeaders, (JSGlobalObject * globalObject, CallFrame* callFrame)) { auto& vm = globalObject->vm(); @@ -500,4 +871,13 @@ JSValue createNodeHTTPInternalBinding(Zig::GlobalObject* globalObject) return obj; } +extern "C" void WebCore__FetchHeaders__toUWSResponse(WebCore::FetchHeaders* arg0, bool is_ssl, void* arg2) +{ + if (is_ssl) { + writeFetchHeadersToUWSResponse(*arg0, reinterpret_cast*>(arg2)); + } else { + writeFetchHeadersToUWSResponse(*arg0, reinterpret_cast*>(arg2)); + } +} + } // namespace Bun diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index fb4c7769cad3e..273857fbbda73 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -136,65 +136,6 @@ static WTF::StringView StringView_slice(WTF::StringView sv, unsigned start, unsi return sv.substring(start, end - start); } -template -static void writeResponseHeader(UWSResponse* res, const WTF::StringView& name, const WTF::StringView& value) -{ - WTF::CString nameStr; - WTF::CString valueStr; - - std::string_view nameView; - std::string_view valueView; - - if (name.is8Bit()) { - const auto nameSpan = name.span8(); - nameView = std::string_view(reinterpret_cast(nameSpan.data()), nameSpan.size()); - } else { - nameStr = name.utf8(); - nameView = std::string_view(nameStr.data(), nameStr.length()); - } - - if (value.is8Bit()) { - const auto valueSpan = value.span8(); - valueView = std::string_view(reinterpret_cast(valueSpan.data()), valueSpan.size()); - } else { - valueStr = value.utf8(); - valueView = std::string_view(valueStr.data(), valueStr.length()); - } - - res->writeHeader(nameView, valueView); -} - -template -static void copyToUWS(WebCore::FetchHeaders* headers, UWSResponse* res) -{ - auto& internalHeaders = headers->internalHeaders(); - - for (auto& value : internalHeaders.getSetCookieHeaders()) { - - if (value.is8Bit()) { - const auto valueSpan = value.span8(); - res->writeHeader(std::string_view("set-cookie", 10), std::string_view(reinterpret_cast(valueSpan.data()), valueSpan.size())); - } else { - WTF::CString valueStr = value.utf8(); - res->writeHeader(std::string_view("set-cookie", 10), std::string_view(valueStr.data(), valueStr.length())); - } - } - - for (const auto& header : internalHeaders.commonHeaders()) { - const auto& name = WebCore::httpHeaderNameString(header.key); - const auto& value = header.value; - - writeResponseHeader(res, name, value); - } - - for (auto& header : internalHeaders.uncommonHeaders()) { - const auto& name = header.key; - const auto& value = header.value; - - writeResponseHeader(res, name, value); - } -} - using namespace JSC; using namespace WebCore; @@ -1364,15 +1305,6 @@ bool WebCore__FetchHeaders__isEmpty(WebCore__FetchHeaders* arg0) return arg0->size() == 0; } -void WebCore__FetchHeaders__toUWSResponse(WebCore__FetchHeaders* arg0, bool is_ssl, void* arg2) -{ - if (is_ssl) { - copyToUWS>(arg0, reinterpret_cast*>(arg2)); - } else { - copyToUWS>(arg0, reinterpret_cast*>(arg2)); - } -} - WebCore__FetchHeaders* WebCore__FetchHeaders__createEmpty() { auto* headers = new WebCore::FetchHeaders({ WebCore::FetchHeaders::Guard::None, {} }); diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index f32a6a9b19b00..192317c0f60d3 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -76,4 +76,5 @@ pub const Classes = struct { pub const TextEncoderStreamEncoder = JSC.WebCore.TextEncoderStreamEncoder; pub const NativeZlib = JSC.API.NativeZlib; pub const NativeBrotli = JSC.API.NativeBrotli; + pub const NodeHTTPResponse = JSC.API.NodeHTTPResponse; }; diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index 56de7842c7d22..228ca3f2199bc 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -2131,7 +2131,7 @@ pub fn HTTPServerWritable(comptime ssl: bool) type { this.res.end(buf, false); this.has_backpressure = false; } else { - this.has_backpressure = !this.res.write(buf); + this.has_backpressure = this.res.write(buf) == .backpressure; } this.handleWrote(buf.len); return true; diff --git a/src/bun.zig b/src/bun.zig index 928a260083dd5..9f14a84182400 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -3132,8 +3132,8 @@ pub fn NewRefCounted(comptime T: type, comptime deinit_fn: ?fn (self: *T) void) const ptr = bun.new(T, t); if (Environment.enable_logs) { - if (ptr.ref_count != 1) { - Output.panic("Expected ref_count to be 1, got {d}", .{ptr.ref_count}); + if (ptr.ref_count == 0) { + Output.panic("Expected ref_count to be > 0, got {d}", .{ptr.ref_count}); } } diff --git a/src/deps/libuwsockets.cpp b/src/deps/libuwsockets.cpp index bc9ff248f8c23..a4af0b0673fb7 100644 --- a/src/deps/libuwsockets.cpp +++ b/src/deps/libuwsockets.cpp @@ -1241,17 +1241,17 @@ extern "C" } } - bool uws_res_write(int ssl, uws_res_r res, const char *data, size_t length) nonnull_fn_decl; + bool uws_res_write(int ssl, uws_res_r res, const char *data, size_t *length) nonnull_fn_decl; - bool uws_res_write(int ssl, uws_res_r res, const char *data, size_t length) + bool uws_res_write(int ssl, uws_res_r res, const char *data, size_t *length) { if (ssl) { uWS::HttpResponse *uwsRes = (uWS::HttpResponse *)res; - return uwsRes->write(std::string_view(data, length)); + return uwsRes->write(std::string_view(data, *length), length); } uWS::HttpResponse *uwsRes = (uWS::HttpResponse *)res; - return uwsRes->write(std::string_view(data, length)); + return uwsRes->write(std::string_view(data, *length), length); } uint64_t uws_res_get_write_offset(int ssl, uws_res_r res) nonnull_fn_decl; uint64_t uws_res_get_write_offset(int ssl, uws_res_r res) diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 102858501ac26..6d554ae41f4de 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -17,6 +17,10 @@ const SSLWrapper = @import("../bun.js/api/bun/ssl_wrapper.zig").SSLWrapper; const TextEncoder = @import("../bun.js/webcore/encoding.zig").Encoder; const JSC = bun.JSC; const EventLoopTimer = @import("../bun.js//api//Timer.zig").EventLoopTimer; +const WriteResult = union(enum) { + want_more: usize, + backpressure: usize, +}; pub const CloseCode = enum(i32) { normal = 0, @@ -3036,6 +3040,27 @@ pub const AnyResponse = union(enum) { SSL: *NewApp(true).Response, TCP: *NewApp(false).Response, + pub fn getWriteOffset(this: AnyResponse) u64 { + return switch (this) { + .SSL => |resp| resp.getWriteOffset(), + .TCP => |resp| resp.getWriteOffset(), + }; + } + + pub fn writeContinue(this: AnyResponse) void { + return switch (this) { + .SSL => |resp| resp.writeContinue(), + .TCP => |resp| resp.writeContinue(), + }; + } + + pub fn state(this: AnyResponse) State { + return switch (this) { + .SSL => |resp| resp.state(), + .TCP => |resp| resp.state(), + }; + } + pub fn timeout(this: AnyResponse, seconds: u8) void { switch (this) { .SSL => |resp| resp.timeout(seconds), @@ -3043,6 +3068,16 @@ pub const AnyResponse = union(enum) { } } + pub fn onData(this: AnyResponse, comptime UserDataType: type, comptime handler: fn (UserDataType, []const u8, bool) void, opcional_data: UserDataType) void { + return switch (this) { + inline .SSL, .TCP => |resp, ssl| resp.onData(UserDataType, struct { + pub fn onDataCallback(user_data: UserDataType, _: *uws.NewApp(ssl == .SSL).Response, data: []const u8, last: bool) void { + @call(.always_inline, handler, .{ user_data, data, last }); + } + }.onDataCallback, opcional_data), + }; + } + pub fn writeStatus(this: AnyResponse, status: []const u8) void { return switch (this) { .SSL => |resp| resp.writeStatus(status), @@ -3057,7 +3092,7 @@ pub const AnyResponse = union(enum) { }; } - pub fn write(this: AnyResponse, data: []const u8) void { + pub fn write(this: AnyResponse, data: []const u8) WriteResult { return switch (this) { .SSL => |resp| resp.write(data), .TCP => |resp| resp.write(data), @@ -3526,8 +3561,12 @@ pub fn NewApp(comptime ssl: bool) type { pub fn resetTimeout(res: *Response) void { uws_res_reset_timeout(ssl_flag, res.downcast()); } - pub fn write(res: *Response, data: []const u8) bool { - return uws_res_write(ssl_flag, res.downcast(), data.ptr, data.len); + pub fn write(res: *Response, data: []const u8) WriteResult { + var len: usize = data.len; + return switch (uws_res_write(ssl_flag, res.downcast(), data.ptr, &len)) { + true => .{ .want_more = len }, + false => .{ .backpressure = len }, + }; } pub fn getWriteOffset(res: *Response) u64 { return uws_res_get_write_offset(ssl_flag, res.downcast()); @@ -3952,7 +3991,7 @@ extern fn uws_res_end_without_body(ssl: i32, res: *uws_res, close_connection: bo extern fn uws_res_end_sendfile(ssl: i32, res: *uws_res, write_offset: u64, close_connection: bool) void; extern fn uws_res_timeout(ssl: i32, res: *uws_res, timeout: u8) void; extern fn uws_res_reset_timeout(ssl: i32, res: *uws_res) void; -extern fn uws_res_write(ssl: i32, res: *uws_res, data: [*c]const u8, length: usize) bool; +extern fn uws_res_write(ssl: i32, res: *uws_res, data: ?[*]const u8, length: *usize) bool; extern fn uws_res_get_write_offset(ssl: i32, res: *uws_res) u64; extern fn uws_res_override_write_offset(ssl: i32, res: *uws_res, u64) void; extern fn uws_res_has_responded(ssl: i32, res: *uws_res) bool; diff --git a/src/js/builtins/ProcessObjectInternals.ts b/src/js/builtins/ProcessObjectInternals.ts index b62f3028b3332..8616d19629d3d 100644 --- a/src/js/builtins/ProcessObjectInternals.ts +++ b/src/js/builtins/ProcessObjectInternals.ts @@ -299,7 +299,7 @@ export function initializeNextTickQueue(process, nextTickQueue, drainMicrotasksF setup = undefined; }; - function nextTick(cb, args) { + function nextTick(cb, ...args) { validateFunction(cb, "callback"); if (setup) { setup(); @@ -309,7 +309,9 @@ export function initializeNextTickQueue(process, nextTickQueue, drainMicrotasksF queue.push({ callback: cb, - args: $argumentCount() > 1 ? Array.prototype.slice.$call(arguments, 1) : undefined, + // We want to avoid materializing the args if there are none because it's + // a waste of memory and Array.prototype.slice shows up in profiling. + args: $argumentCount() > 1 ? args : undefined, frame: $getInternalField($asyncContext, 0), }); $putInternalField(nextTickQueue, 0, 1); diff --git a/src/js/node/http.ts b/src/js/node/http.ts index a0be75f7343b2..e76c1155bdcd3 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -1,4 +1,16 @@ -// Hardcoded module "node:http" +const enum NodeHTTPIncomingRequestType { + request, + response, + onNodeHTTPRequest, +} +const enum NodeHTTPHeaderState { + none, + assigned, + sent, +} + +const headerStateSymbol = Symbol("headerState"); + const EventEmitter = require("node:events"); const { isTypedArray } = require("node:util/types"); const { Duplex, Readable, Writable } = require("node:stream"); @@ -67,11 +79,11 @@ const validateHeaderName = (name, label) => { const validateHeaderValue = (name, value) => { if (value === undefined) { // throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name); - throw new Error("ERR_HTTP_INVALID_HEADER_VALUE"); + throw $ERR_HTTP_INVALID_HEADER_VALUE(`Invalid header value: ${value} for ${name}`); } if (checkInvalidHeaderChar(value)) { // throw new ERR_INVALID_CHAR("header content", name); - throw new Error("ERR_INVALID_CHAR"); + throw $ERR_INVALID_CHAR(`Invalid header value: ${value} for ${name}`); } }; @@ -634,6 +646,32 @@ Server.prototype = { }, }, maxRequestBodySize: Number.MAX_SAFE_INTEGER, + + onNodeHTTPRequest( + bunServer, + url: string, + method: string, + headersObject: Record, + headersArray: string[], + handle, + hasBody: boolean, + ) { + const http_req = new RequestClass( + kInternalRequest, + url, + method, + headersObject, + headersArray, + handle, + hasBody, + ); + const http_res = new ResponseClass(http_req, { + [kHandle]: handle, + }); + + server.emit("request", http_req, http_res); + }, + // Be very careful not to access (web) Request object // properties: // - request.url @@ -664,7 +702,7 @@ Server.prototype = { const upgrade = http_req.headers.upgrade; - const http_res = new ResponseClass(http_req, reply); + const http_res = new ResponseClass(http_req, { [kDeprecatedReplySymbol]: reply }); http_req.socket[kInternalSocketData] = [server, http_res, req]; server.emit("connection", http_req.socket); @@ -792,67 +830,105 @@ var reqSymbol = Symbol("req"); var bodyStreamSymbol = Symbol("bodyStream"); var noBodySymbol = Symbol("noBody"); var abortedSymbol = Symbol("aborted"); +const isInternalRequestSymbol = Symbol("isInternalRequest"); + function IncomingMessage(req, defaultIncomingOpts) { - this.method = null; + this[abortedSymbol] = false; this._consuming = false; this._dumped = false; - this[noBodySymbol] = false; - this[abortedSymbol] = false; this.complete = false; - Readable.$call(this); - var { type = "request", [kInternalRequest]: nodeReq } = defaultIncomingOpts || {}; - this[reqSymbol] = req; - this[typeSymbol] = type; + // (url, method, headers, rawHeaders, handle, hasBody) + if (req === kInternalRequest) { + this.url = arguments[1]; + this.method = arguments[2]; + this.headers = arguments[3]; + this.rawHeaders = arguments[4]; + this[kInternalRequest] = arguments[5]; + this[noBodySymbol] = !arguments[6]; + Readable.$call(this); + } else { + this.method = null; + this[noBodySymbol] = false; + Readable.$call(this); + var { [typeSymbol]: type = NodeHTTPIncomingRequestType.request, [kInternalRequest]: nodeReq } = + defaultIncomingOpts || {}; - this[bodyStreamSymbol] = undefined; + this[reqSymbol] = req; + this[typeSymbol] = type; - this.req = nodeReq; + this[bodyStreamSymbol] = undefined; - if (!assignHeaders(this, req)) { - this[fakeSocketSymbol] = req; - const reqUrl = String(req?.url || ""); - this.url = reqUrl; - } + this.req = nodeReq; + this.req = nodeReq; - if (isNextIncomingMessageHTTPS) { - // Creating a new Duplex is expensive. - // We can skip it if the request is not HTTPS. - const socket = new FakeSocket(); - this[fakeSocketSymbol] = socket; - socket.encrypted = true; - isNextIncomingMessageHTTPS = false; - } + this.req = nodeReq; + + if (!assignHeaders(this, req)) { + this[fakeSocketSymbol] = req; + const reqUrl = String(req?.url || ""); + this.url = reqUrl; + } + + if (isNextIncomingMessageHTTPS) { + // Creating a new Duplex is expensive. + // We can skip it if the request is not HTTPS. + const socket = new FakeSocket(); + this[fakeSocketSymbol] = socket; + socket.encrypted = true; + isNextIncomingMessageHTTPS = false; + } - this[noBodySymbol] = - type === "request" // TODO: Add logic for checking for body on response - ? requestHasNoBody(this.method, this) - : false; + this[noBodySymbol] = + type === NodeHTTPIncomingRequestType.request // TODO: Add logic for checking for body on response + ? requestHasNoBody(this.method, this) + : false; + } } IncomingMessage.prototype = { constructor: IncomingMessage, + __proto__: Readable.prototype, _construct(callback) { // TODO: streaming - if (this[typeSymbol] === "response" || this[noBodySymbol]) { + const type = this[typeSymbol]; + if (type === NodeHTTPIncomingRequestType.response || this[noBodySymbol]) { callback(); return; } - const contentLength = this.headers["content-length"]; - const length = contentLength ? parseInt(contentLength, 10) : 0; - if (length === 0) { - this[noBodySymbol] = true; + if (type !== NodeHTTPIncomingRequestType.onNodeHTTPRequest) { + const contentLength = this.headers["content-length"]; + const length = contentLength ? parseInt(contentLength, 10) : 0; + if (length === 0) { + this[noBodySymbol] = true; + callback(); + return; + } + callback(); - return; } - - callback(); }, _read(size) { + let internalRequest; if (this[noBodySymbol]) { this.complete = true; this.push(null); + } else if ((internalRequest = this[kInternalRequest])) { + internalRequest.ondata = (chunk, isLast, isAborted) => { + console.log("ondata", chunk, isLast, isAborted); + if (isAborted) { + if (!this.destroyed) { + this.destroy(); + } + return; + } + this.push(chunk); + if (isLast) { + this.complete = true; + this.push(null); + } + }; } else if (this[bodyStreamSymbol] == null) { const reader = this[reqSymbol].body?.getReader() as ReadableStreamDefaultReader; if (!reader) { @@ -864,6 +940,9 @@ IncomingMessage.prototype = { consumeStream(this, reader); } }, + _finish() { + this.emit("prefinish"); + }, _destroy(err, cb) { if (!this.readableEnded || !this.complete) { this[abortedSymbol] = true; @@ -877,12 +956,18 @@ IncomingMessage.prototype = { err = undefined; } - const stream = this[bodyStreamSymbol]; - this[bodyStreamSymbol] = undefined; - const streamState = stream?.$state; + var internalRequest = this[kInternalRequest]; + if (internalRequest) { + this[kInternalRequest] = undefined; + internalRequest.ondata = undefined; + } else { + const stream = this[bodyStreamSymbol]; + this[bodyStreamSymbol] = undefined; + const streamState = stream?.$state; - if (streamState === $streamReadable || streamState === $streamWaiting || streamState === $streamWritable) { - stream?.cancel?.().catch(nop); + if (streamState === $streamReadable || streamState === $streamWaiting || streamState === $streamWritable) { + stream?.cancel?.().catch(nop); + } } const socket = this[fakeSocketSymbol]; @@ -961,7 +1046,6 @@ IncomingMessage.prototype = { this[fakeSocketSymbol] = value; }, }; -$setPrototypeDirect.$call(IncomingMessage.prototype, Readable.prototype); $setPrototypeDirect.$call(IncomingMessage, Readable); async function consumeStream(self, reader: ReadableStreamDefaultReader) { @@ -1010,154 +1094,133 @@ function OutgoingMessage(options) { this.headersSent = false; this.sendDate = true; this[finishedSymbol] = false; - this[kEndCalled] = false; + this[headerStateSymbol] = NodeHTTPHeaderState.none; this[kAbortController] = null; } -$setPrototypeDirect.$call((OutgoingMessage.prototype = {}), Writable.prototype); -OutgoingMessage.prototype.constructor = OutgoingMessage; // Re-add constructor which got lost when setting prototype -$setPrototypeDirect.$call(OutgoingMessage, Writable); - -// Express "compress" package uses this -OutgoingMessage.prototype._implicitHeader = function () {}; - -OutgoingMessage.prototype.appendHeader = function (name, value) { - var headers = (this[headersSymbol] ??= new Headers()); - headers.append(name, value); -}; - -OutgoingMessage.prototype.flushHeaders = function () {}; - -OutgoingMessage.prototype.getHeader = function (name) { - return getHeader(this[headersSymbol], name); -}; +const OutgoingMessagePrototype = { + constructor: OutgoingMessage, + __proto__: Writable.prototype, -OutgoingMessage.prototype.getHeaders = function () { - if (!this[headersSymbol]) return kEmptyObject; - return this[headersSymbol].toJSON(); -}; - -OutgoingMessage.prototype.getHeaderNames = function () { - var headers = this[headersSymbol]; - if (!headers) return []; - return Array.from(headers.keys()); -}; - -OutgoingMessage.prototype.removeHeader = function (name) { - if (!this[headersSymbol]) return; - this[headersSymbol].delete(name); -}; - -OutgoingMessage.prototype.setHeader = function (name, value) { - this[headersSymbol] = this[headersSymbol] ?? new Headers(); - var headers = this[headersSymbol]; - headers.set(name, value); - return this; -}; - -OutgoingMessage.prototype.hasHeader = function (name) { - if (!this[headersSymbol]) return false; - return this[headersSymbol].has(name); -}; + appendHeader(name, value) { + var headers = (this[headersSymbol] ??= new Headers()); + headers.append(name, value); + return this; + }, -OutgoingMessage.prototype.addTrailers = function (headers) { - throw new Error("not implemented"); -}; + _implicitHeader() { + throw $ERR_METHOD_NOT_IMPLEMENTED("The method _implicitHeader() is not implemented"); + }, + flushHeaders() {}, + getHeader(name) { + return getHeader(this[headersSymbol], name); + }, + getHeaders() { + if (!this[headersSymbol]) return kEmptyObject; + return this[headersSymbol].toJSON(); + }, + getHeaderNames() { + var headers = this[headersSymbol]; + if (!headers) return []; + return Array.from(headers.keys()); + }, + removeHeader(name) { + if (!this[headersSymbol]) return; + this[headersSymbol].delete(name); + }, + setHeader(name, value) { + this[headersSymbol] = this[headersSymbol] ?? new Headers(); + var headers = this[headersSymbol]; + headers.set(name, value); + return this; + }, + hasHeader(name) { + if (!this[headersSymbol]) return false; + return this[headersSymbol].has(name); + }, + addTrailers(headers) { + throw new Error("not implemented"); + }, -function onTimeout() { - this[timeoutTimerSymbol] = undefined; - this[kAbortController]?.abort(); - this.emit("timeout"); -} + get headers() { + if (!this[headersSymbol]) return kEmptyObject; + return this[headersSymbol].toJSON(); + }, + set headers(value) { + this[headersSymbol] = new Headers(value); + }, -OutgoingMessage.prototype.setTimeout = function (msecs, callback) { - if (this.destroyed) return this; + setTimeout(msecs, callback) { + if (this.destroyed) return this; - this.timeout = msecs = validateMsecs(msecs, "msecs"); + this.timeout = msecs = validateMsecs(msecs, "msecs"); - // Attempt to clear an existing timer in both cases - - // even if it will be rescheduled we don't want to leak an existing timer. - clearTimeout(this[timeoutTimerSymbol]); + // Attempt to clear an existing timer in both cases - + // even if it will be rescheduled we don't want to leak an existing timer. + clearTimeout(this[timeoutTimerSymbol]); - if (msecs === 0) { - if (callback !== undefined) { - validateFunction(callback, "callback"); - this.removeListener("timeout", callback); - } + if (msecs === 0) { + if (callback !== undefined) { + validateFunction(callback, "callback"); + this.removeListener("timeout", callback); + } - this[timeoutTimerSymbol] = undefined; - } else { - this[timeoutTimerSymbol] = setTimeout(onTimeout.bind(this), msecs).unref(); + this[timeoutTimerSymbol] = undefined; + } else { + this[timeoutTimerSymbol] = setTimeout(onTimeout.bind(this), msecs).unref(); - if (callback !== undefined) { - validateFunction(callback, "callback"); - this.once("timeout", callback); + if (callback !== undefined) { + validateFunction(callback, "callback"); + this.once("timeout", callback); + } } - } - - return this; -}; -Object.defineProperty(OutgoingMessage.prototype, "headers", { - // For compat with IncomingRequest - get: function () { - if (!this[headersSymbol]) return kEmptyObject; - return this[headersSymbol].toJSON(); + return this; }, -}); -Object.defineProperty(OutgoingMessage.prototype, "chunkedEncoding", { - get: function () { - return false; + get writableEnded() { + return this[finishedSymbol]; }, - set: function (value) { - // throw new Error('not implemented'); + get connection() { + return this.socket; }, -}); -Object.defineProperty(OutgoingMessage.prototype, "shouldKeepAlive", { - get: function () { - return true; + get socket() { + this[fakeSocketSymbol] = this[fakeSocketSymbol] ?? new FakeSocket(); + return this[fakeSocketSymbol]; }, - set: function (value) { - // throw new Error('not implemented'); + set socket(value) { + this[fakeSocketSymbol] = value; }, -}); -Object.defineProperty(OutgoingMessage.prototype, "useChunkedEncodingByDefault", { - get: function () { + get usesChunkedEncodingByDefault() { return true; }, - set: function (value) { - // throw new Error('not implemented'); + set usesChunkedEncodingByDefault(value) { + // noop }, -}); -Object.defineProperty(OutgoingMessage.prototype, "socket", { - get: function () { - this[fakeSocketSymbol] = this[fakeSocketSymbol] ?? new FakeSocket(); - return this[fakeSocketSymbol]; + get chunkedEncoding() { + return false; }, - set: function (val) { - this[fakeSocketSymbol] = val; + set chunkedEncoding(value) { + // noop }, -}); -Object.defineProperty(OutgoingMessage.prototype, "connection", { - get: function () { - return this.socket; + _send(data, encoding, callback, byteLength) { + return this.write(data, encoding, callback); }, -}); -Object.defineProperty(OutgoingMessage.prototype, "finished", { - get: function () { - return this[finishedSymbol]; + [EventEmitter.captureRejectionSymbol]: function (err, event) { + this.destroy(err); }, -}); +} satisfies typeof import("node:http").OutgoingMessage.prototype; +OutgoingMessage.prototype = OutgoingMessagePrototype; +$setPrototypeDirect.$call(OutgoingMessage, Writable); function emitContinueAndSocketNT(self) { if (self.destroyed) return; @@ -1208,41 +1271,272 @@ let OriginalWriteHeadFn, OriginalImplicitHeadFn; const controllerSymbol = Symbol("controller"); const firstWriteSymbol = Symbol("firstWrite"); const deferredSymbol = Symbol("deferred"); -function ServerResponse(req, reply) { - OutgoingMessage.$call(this, reply); +const kDeprecatedReplySymbol = Symbol("deprecatedReply"); +const kHandle = Symbol("handle"); + +function ServerResponse(req, options) { + if ((this[kDeprecatedReplySymbol] = options?.[kDeprecatedReplySymbol])) { + this[controllerSymbol] = undefined; + this[firstWriteSymbol] = undefined; + this[deferredSymbol] = undefined; + this._writev = ServerResponse_writevDeprecated; + this._write = ServerResponse_writeDeprecated; + this._final = ServerResponse_finalDeprecated; + } + + OutgoingMessage.$call(this, options); + this.req = req; - this._reply = reply; this.sendDate = true; this.statusCode = 200; - this.headersSent = false; this.statusMessage = undefined; - this[controllerSymbol] = undefined; - this[firstWriteSymbol] = undefined; this._writableState.decodeStrings = false; - this[deferredSymbol] = undefined; - this._sent100 = false; this._defaultKeepAlive = false; this._removedConnection = false; this._removedContLen = false; this._hasBody = true; + this[headerStateSymbol] = NodeHTTPHeaderState.none; this[finishedSymbol] = false; // this is matching node's behaviour // https://github.com/nodejs/node/blob/cf8c6994e0f764af02da4fa70bc5962142181bf3/lib/_http_server.js#L192 if (req.method === "HEAD") this._hasBody = false; + + const handle = options?.[kHandle]; + + if (handle) { + this[kHandle] = handle; + handle.onabort = () => this.destroy(); + } } -$setPrototypeDirect.$call((ServerResponse.prototype = {}), OutgoingMessage.prototype); -ServerResponse.prototype.constructor = ServerResponse; // Re-add constructor which got lost when setting prototype -$setPrototypeDirect.$call(ServerResponse, OutgoingMessage); - -// Express "compress" package uses this -ServerResponse.prototype._implicitHeader = function () { - // @ts-ignore - this.writeHead(this.statusCode); -}; +const ServerResponsePrototype = { + constructor: ServerResponse, + __proto__: OutgoingMessage.prototype, + + get headersSent() { + return this[headerStateSymbol] === NodeHTTPHeaderState.assigned; + }, + set headersSent(value) { + this[headerStateSymbol] = value ? NodeHTTPHeaderState.assigned : NodeHTTPHeaderState.none; + }, + + // This end method is actually on the OutgoingMessage prototype in Node.js + // But we don't want it for the fetch() response version. + end(chunk, encoding, callback) { + if (this.destroyed) { + emitErrorNextTick(this, $ERR_STREAM_DESTROYED("Stream is destroyed"), callback); + return this; + } + + const isFinished = this.finished; + + if (isFinished && chunk) { + emitErrorNextTick(this, $ERR_STREAM_WRITE_AFTER_END("Stream is already finished"), callback); + return this; + } + + if (isFinished && $isCallable(callback)) { + emitErrorNextTick(this, $ERR_STREAM_ALREADY_FINISHED("Stream is already finished"), callback); + return this; + } + + const handle = this[kHandle]; + if (handle) { + const headerState = this[headerStateSymbol]; + + if (headerState !== NodeHTTPHeaderState.sent) { + handle.cork(() => { + handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]); + + // If handle.writeHead throws, we don't want headersSent to be set to true. + // So we set it here. + this[headerStateSymbol] = NodeHTTPHeaderState.sent; + + // https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/_http_outgoing.js#L987 + this._contentLength = handle.end(chunk, encoding, callback); + }); + } else { + handle.end(chunk, encoding, callback); + } + } + + return this; + }, + + write(chunk, encoding, callback) { + const handle = this[kHandle]; + if (!handle) { + return OutgoingMessagePrototype.write.$apply(this, arguments); + } + + if (this.destroyed) { + if ($isCallable(callback)) { + callback($ERR_STREAM_DESTROYED("Stream is destroyed")); + } + return false; + } + + if (this.finished) { + emitErrorNextTick(this, $ERR_STREAM_WRITE_AFTER_END("Stream is already finished"), callback); + return false; + } + + let result = 0; + + if (this[headerStateSymbol] !== NodeHTTPHeaderState.sent) { + handle.cork(() => { + handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]); + + // If handle.writeHead throws, we don't want headersSent to be set to true. + // So we set it here. + this[headerStateSymbol] = NodeHTTPHeaderState.sent; + + result = handle.write(chunk, encoding); + }); + } else { + result = handle.write(chunk, encoding); + } + + if (result < 0) { + handle.onwritable = () => { + if (!this.finished && !this.destroyed) { + this.emit("drain"); + } + }; + return false; + } + + this.emit("drain"); + return true; + }, + + appendHeader(name, value) { + const headers = (this[headersSymbol] ??= new Headers()); + headers.append(name, value); + + return this; + }, + + getHeader(name) { + return getHeader(this[headersSymbol], name); + }, + + getHeaders() { + const headers = this[headersSymbol]; + if (!headers) return kEmptyObject; + return headers.toJSON(); + }, + + getHeaderNames() { + const headers = this[headersSymbol]; + if (!headers) return []; + return Array.from(headers.keys()); + }, + + removeHeader(name) { + if (!this[headersSymbol]) return; + this[headersSymbol].delete(name); + }, + + setHeader(name, value) { + this[headersSymbol] = this[headersSymbol] ?? new Headers(); + const headers = this[headersSymbol]; + setHeader(headers, name, value); + return this; + }, + + hasHeader(name) { + if (!this[headersSymbol]) return false; + return this[headersSymbol].has(name); + }, + + _finish() { + OutgoingMessage.prototype._finish.$call(this); + }, -ServerResponse.prototype._write = function (chunk, encoding, callback) { + _implicitHeader() { + // @ts-ignore + this.writeHead(this.statusCode); + }, + + get writableNeedDrain() { + return !this.destroyed && !this[finishedSymbol] && (this[kHandle]?.bufferedAmount ?? 0) === 0; + }, + + _send(data, encoding, callback, byteLength) { + const handle = this[kHandle]; + if (!handle) { + return OutgoingMessagePrototype._send.$apply(this, arguments); + } + + if (this[headerStateSymbol] !== NodeHTTPHeaderState.sent) { + handle.cork(() => { + handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]); + this[headerStateSymbol] = NodeHTTPHeaderState.sent; + handle.write(data, encoding, callback); + }); + } else { + handle.write(data, encoding, callback); + } + }, + + writeHead(statusCode, statusMessage, headers) { + if (this[headerStateSymbol] === NodeHTTPHeaderState.none) { + _writeHead(statusCode, statusMessage, headers, this); + this[headerStateSymbol] = NodeHTTPHeaderState.assigned; + } + + return this; + }, + + assignSocket(socket) { + if (socket._httpMessage) { + throw ERR_HTTP_SOCKET_ASSIGNED(); + } + socket._httpMessage = this; + socket.on("close", () => onServerResponseClose.$call(socket)); + this.socket = socket; + this._writableState.autoDestroy = false; + this.emit("socket", socket); + }, + + statusMessage: undefined, + statusCode: 200, + + get shouldKeepAlive() { + return this[kHandle]?.shouldKeepAlive ?? true; + }, + set shouldKeepAlive(value) { + // throw new Error('not implemented'); + }, + + get chunkedEncoding() { + return false; + }, + set chunkedEncoding(value) { + // throw new Error('not implemented'); + }, + + get useChunkedEncodingByDefault() { + return true; + }, + set useChunkedEncodingByDefault(value) { + // throw new Error('not implemented'); + }, + + flushHeaders() { + this._implicitHeader(); + + const handle = this[kHandle]; + if (handle && !this.headersSent) { + handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]); + } + }, +} satisfies typeof import("node:http").ServerResponse.prototype; +ServerResponse.prototype = ServerResponsePrototype; + +const ServerResponse_writeDeprecated = function _write(chunk, encoding, callback) { if (this[firstWriteSymbol] === undefined && !this.headersSent) { this[firstWriteSymbol] = chunk; callback(); @@ -1255,29 +1549,13 @@ ServerResponse.prototype._write = function (chunk, encoding, callback) { }); }; -ServerResponse.prototype._writev = function (chunks, callback) { - if (chunks.length === 1 && !this.headersSent && this[firstWriteSymbol] === undefined) { - this[firstWriteSymbol] = chunks[0].chunk; - callback(); - return; - } - - ensureReadableStreamController.$call(this, controller => { - for (const chunk of chunks) { - controller.write(chunk.chunk); - } - - callback(); - }); -}; - function ensureReadableStreamController(run) { const thisController = this[controllerSymbol]; if (thisController) return run(thisController); this.headersSent = true; let firstWrite = this[firstWriteSymbol]; this[controllerSymbol] = undefined; - this._reply( + this[kDeprecatedReplySymbol]( new Response( new ReadableStream({ type: "direct", @@ -1310,7 +1588,7 @@ function drainHeadersIfObservable() { this._implicitHeader(); } -ServerResponse.prototype._final = function (callback) { +function ServerResponse_finalDeprecated(callback) { const req = this.req; const shouldEmitClose = req && req.emit && !this[finishedSymbol]; @@ -1320,7 +1598,7 @@ ServerResponse.prototype._final = function (callback) { this[finishedSymbol] = true; this.headersSent = true; // https://github.com/oven-sh/bun/issues/3458 drainHeadersIfObservable.$call(this); - this._reply( + this[kDeprecatedReplySymbol]( new Response(data, { headers: this[headersSymbol], status: this.statusCode, @@ -1349,113 +1627,11 @@ ServerResponse.prototype._final = function (callback) { deferred(); } }); -}; - -ServerResponse.prototype.writeProcessing = function () { - throw new Error("not implemented"); -}; - -ServerResponse.prototype.addTrailers = function (headers) { - throw new Error("not implemented"); -}; - -ServerResponse.prototype.assignSocket = function (socket) { - if (socket._httpMessage) { - throw ERR_HTTP_SOCKET_ASSIGNED(); - } - socket._httpMessage = this; - socket.on("close", () => onServerResponseClose.$call(socket)); - this.socket = socket; - this._writableState.autoDestroy = false; - this.emit("socket", socket); -}; - -ServerResponse.prototype.detachSocket = function (socket) { - throw new Error("not implemented"); -}; - -ServerResponse.prototype.writeContinue = function (callback) { - throw new Error("not implemented"); -}; - -ServerResponse.prototype.setTimeout = function (msecs, callback) { - // TODO: - return this; -}; - -ServerResponse.prototype.appendHeader = function (name, value) { - this[headersSymbol] = this[headersSymbol] ?? new Headers(); - const headers = this[headersSymbol]; - headers.append(name, value); -}; - -ServerResponse.prototype.flushHeaders = function () {}; - -ServerResponse.prototype.getHeader = function (name) { - return getHeader(this[headersSymbol], name); -}; - -ServerResponse.prototype.getHeaders = function () { - const headers = this[headersSymbol]; - if (!headers) return kEmptyObject; - return headers.toJSON(); -}; - -ServerResponse.prototype.getHeaderNames = function () { - const headers = this[headersSymbol]; - if (!headers) return []; - return Array.from(headers.keys()); -}; - -ServerResponse.prototype.removeHeader = function (name) { - if (!this[headersSymbol]) return; - this[headersSymbol].delete(name); -}; - -ServerResponse.prototype.setHeader = function (name, value) { - this[headersSymbol] = this[headersSymbol] ?? new Headers(); - const headers = this[headersSymbol]; - setHeader(headers, name, value); - return this; -}; - -ServerResponse.prototype.hasHeader = function (name) { - if (!this[headersSymbol]) return false; - return this[headersSymbol].has(name); -}; - -ServerResponse.prototype.writeHead = function (statusCode, statusMessage, headers) { - _writeHead(statusCode, statusMessage, headers, this); - - return this; -}; - -Object.defineProperty(ServerResponse.prototype, "shouldKeepAlive", { - get() { - return true; - }, - set(value) { - // throw new Error('not implemented'); - }, -}); +} -Object.defineProperty(ServerResponse.prototype, "chunkedEncoding", { - get() { - return false; - }, - set(value) { - // throw new Error('not implemented'); - }, -}); +// ServerResponse.prototype._final = ServerResponse_finalDeprecated; -Object.defineProperty(ServerResponse.prototype, "useChunkedEncodingByDefault", { - get() { - return true; - }, - set(value) { - // throw new Error('not implemented'); - }, -}); +ServerResponse.prototype.writeHeader = ServerResponse.prototype.writeHead; OriginalWriteHeadFn = ServerResponse.prototype.writeHead; OriginalImplicitHeadFn = ServerResponse.prototype._implicitHeader; @@ -1636,7 +1812,7 @@ class ClientRequest extends OutgoingMessage { const prevIsHTTPS = isNextIncomingMessageHTTPS; isNextIncomingMessageHTTPS = response.url.startsWith("https:"); var res = (this.#res = new IncomingMessage(response, { - type: "response", + type: NodeHTTPIncomingRequestType.response, [kInternalRequest]: this, })); isNextIncomingMessageHTTPS = prevIsHTTPS; @@ -1667,7 +1843,7 @@ class ClientRequest extends OutgoingMessage { } get aborted() { - return this[abortedSymbol] || this.#signal?.aborted || !!this[kAbortController]?.signal.aborted; + return this[abortedSymbol] || this.#signal?.aborted || !!this[kAbortController]?.signal?.aborted; } set aborted(value) { @@ -2178,7 +2354,7 @@ function _normalizeArgs(args) { function _writeHead(statusCode, reason, obj, response) { statusCode |= 0; if (statusCode < 100 || statusCode > 999) { - throw new Error("status code must be between 100 and 999"); + throw $ERR_HTTP_INVALID_STATUS_CODE(`Invalid status code: ${statusCode}`); } if (typeof reason === "string") { @@ -2233,6 +2409,22 @@ function _writeHead(statusCode, reason, obj, response) { } } +function ServerResponse_writevDeprecated(chunks, callback) { + if (chunks.length === 1 && !this.headersSent && this[firstWriteSymbol] === undefined) { + this[firstWriteSymbol] = chunks[0].chunk; + callback(); + return; + } + + ensureReadableStreamController.$call(this, controller => { + for (const chunk of chunks) { + controller.write(chunk.chunk); + } + + callback(); + }); +} + /** * Makes an HTTP request. * @param {string | URL} url diff --git a/src/jsc.zig b/src/jsc.zig index 8e4c8072649ec..11865ba754a6f 100644 --- a/src/jsc.zig +++ b/src/jsc.zig @@ -29,6 +29,7 @@ pub const Jest = @import("./bun.js/test/jest.zig"); pub const Expect = @import("./bun.js/test/expect.zig"); pub const Snapshot = @import("./bun.js/test/snapshot.zig"); pub const API = struct { + pub const NodeHTTPResponse = @import("./bun.js/api/server.zig").NodeHTTPResponse; pub const Glob = @import("./bun.js/api/glob.zig"); pub const Shell = @import("./shell/shell.zig"); pub const JSBundler = @import("./bun.js/api/JSBundler.zig").JSBundler; From deb44aa80bb5798eedf7e8eb408a128db9f79934 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 6 Oct 2024 07:11:18 -0700 Subject: [PATCH 02/36] okay fastify and express seem to work again --- bench/package.json | 1 + bench/snippets/express-hello.mjs | 13 ++++++++++++ bench/snippets/fastify.mjs | 20 ++++++++++++++++++ packages/bun-uws/src/HttpResponse.h | 13 ++++++++---- src/bun.js/api/server.zig | 13 ++++++------ src/bun.js/bindings/NodeHTTP.cpp | 32 +++++++++++++++++++++++++++++ src/js/node/http.ts | 17 +++++++++++---- 7 files changed, 94 insertions(+), 15 deletions(-) create mode 100644 bench/snippets/express-hello.mjs create mode 100644 bench/snippets/fastify.mjs diff --git a/bench/package.json b/bench/package.json index 11b0ab69bd1af..69be623fdf1f2 100644 --- a/bench/package.json +++ b/bench/package.json @@ -12,6 +12,7 @@ "eventemitter3": "^5.0.0", "execa": "^8.0.1", "fast-glob": "3.3.1", + "fastify": "^5.0.0", "fdir": "^6.1.0", "mitata": "^0.1.6", "string-width": "7.1.0", diff --git a/bench/snippets/express-hello.mjs b/bench/snippets/express-hello.mjs new file mode 100644 index 0000000000000..4c9aea40c7287 --- /dev/null +++ b/bench/snippets/express-hello.mjs @@ -0,0 +1,13 @@ +import express from "express"; + +const app = express(); +const port = 3000; + +var i = 0; +app.get("/", (req, res) => { + res.send("Hello World!" + i++); +}); + +app.listen(port, () => { + console.log(`Express app listening at http://localhost:${port}`); +}); diff --git a/bench/snippets/fastify.mjs b/bench/snippets/fastify.mjs new file mode 100644 index 0000000000000..576a124558ca7 --- /dev/null +++ b/bench/snippets/fastify.mjs @@ -0,0 +1,20 @@ +import Fastify from "fastify"; + +const fastify = Fastify({ + logger: false, +}); + +fastify.get("/", async (request, reply) => { + return { hello: "world" }; +}); + +const start = async () => { + try { + await fastify.listen({ port: 3000 }); + } catch (err) { + fastify.log.error(err); + process.exit(1); + } +}; + +start(); diff --git a/packages/bun-uws/src/HttpResponse.h b/packages/bun-uws/src/HttpResponse.h index 1632266daee8c..5cab0fa3d33d9 100644 --- a/packages/bun-uws/src/HttpResponse.h +++ b/packages/bun-uws/src/HttpResponse.h @@ -440,7 +440,7 @@ struct HttpResponse : public AsyncSocket { bool sendTerminatingChunk(bool closeConnection = false) { writeStatus(HTTP_200_OK); HttpResponseData *httpResponseData = getHttpResponseData(); - if (!(httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED)) { + if (!(httpResponseData->state & (HttpResponseData::HTTP_WRITE_CALLED | HttpResponseData::HTTP_END_CALLED))) { /* Write mark on first call to write */ writeMark(); @@ -469,7 +469,7 @@ struct HttpResponse : public AsyncSocket { HttpResponseData *httpResponseData = getHttpResponseData(); - if (!(httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED)) { + if (!(httpResponseData->state & (HttpResponseData::HTTP_WRITE_CALLED | HttpResponseData::HTTP_END_CALLED))) { /* Write mark on first call to write */ writeMark(); @@ -477,9 +477,14 @@ struct HttpResponse : public AsyncSocket { httpResponseData->state |= HttpResponseData::HTTP_WRITE_CALLED; } + Super::write("\r\n", 2); - writeUnsignedHex((unsigned int) data.length()); - Super::write("\r\n", 2); + + // This happens if they call send, include a content-length header + if (!(httpResponseData->state & HttpResponseData::HTTP_END_CALLED)) { + writeUnsignedHex((unsigned int) data.length()); + Super::write("\r\n", 2); + } auto [written, failed] = Super::write(data.data(), (int) data.length()); /* Reset timeout on each sended chunk */ diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 1078d33a2e601..24d82309d5883 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -5875,7 +5875,7 @@ pub const NodeHTTPResponse = struct { } fn handleEndedIfNecessary(state: uws.State, globalObject: *JSC.JSGlobalObject) bool { - if (state.isHttpEndCalled()) { + if (!state.isResponsePending()) { globalObject.ERR_HTTP_HEADERS_SENT("Stream is already ended", .{}).throw(); return true; } @@ -6110,7 +6110,7 @@ pub const NodeHTTPResponse = struct { } const state = this.response.state(); - if (state.isHttpEndCalled()) { + if (!state.isResponsePending()) { globalObject.ERR_STREAM_WRITE_AFTER_END("Stream already ended", .{}).throw(); return .zero; } @@ -6118,7 +6118,7 @@ pub const NodeHTTPResponse = struct { const input_value = if (arguments.len > 0) arguments[0] else .undefined; var encoding_value = if (arguments.len > 1) arguments[1] else .undefined; const callback_value = brk: { - if (encoding_value != .undefined and encoding_value.isCallable(globalObject.vm())) { + if ((encoding_value != .null and encoding_value != .undefined) and encoding_value.isCallable(globalObject.vm())) { encoding_value = .undefined; break :brk arguments[1]; } @@ -6135,9 +6135,8 @@ pub const NodeHTTPResponse = struct { }; const string_or_buffer: JSC.Node.StringOrBuffer = brk: { - if (input_value == .null) { - globalObject.ERR_STREAM_NULL_VALUES("Cannot write null value to stream", .{}).throw(); - return .zero; + if (input_value == .null or input_value == .undefined) { + break :brk JSC.Node.StringOrBuffer.empty; } var encoding: JSC.Node.Encoding = .utf8; @@ -6177,7 +6176,7 @@ pub const NodeHTTPResponse = struct { } if (is_end and bytes.len == 0 and write_offset == 0) { - this.response.endWithoutBody(state.isHttpConnectionClose()); + this.response.endStream(state.isHttpConnectionClose()); this.onRequestComplete(); return JSC.JSValue.jsNumberFromInt32(0); } diff --git a/src/bun.js/bindings/NodeHTTP.cpp b/src/bun.js/bindings/NodeHTTP.cpp index e0d219827e64d..7ce532ea42f34 100644 --- a/src/bun.js/bindings/NodeHTTP.cpp +++ b/src/bun.js/bindings/NodeHTTP.cpp @@ -504,10 +504,42 @@ static void writeFetchHeadersToUWSResponse(WebCore::FetchHeaders& headers, uWS:: } } + auto* data = res->getHttpResponseData(); + for (const auto& header : internalHeaders.commonHeaders()) { + const auto& name = WebCore::httpHeaderNameString(header.key); const auto& value = header.value; + // We have to tell uWS not to automatically insert a TransferEncoding or Date header. + // Otherwise, you get this when using Fastify; + // + // ❯ curl http://localhost:3000 --verbose + // * Trying [::1]:3000... + // * Connected to localhost (::1) port 3000 + // > GET / HTTP/1.1 + // > Host: localhost:3000 + // > User-Agent: curl/8.4.0 + // > Accept: */* + // > + // < HTTP/1.1 200 OK + // < Content-Type: application/json; charset=utf-8 + // < Content-Length: 17 + // < Date: Sun, 06 Oct 2024 13:37:01 GMT + // < Transfer-Encoding: chunked + // < + // + if (header.key == WebCore::HTTPHeaderName::ContentLength) { + if (!(data->state & uWS::HttpResponseData::HTTP_END_CALLED)) { + data->state |= uWS::HttpResponseData::HTTP_END_CALLED; + res->writeMark(); + } + } else if (header.key == WebCore::HTTPHeaderName::TransferEncoding || header.key == WebCore::HTTPHeaderName::Date) { + if (!(data->state & uWS::HttpResponseData::HTTP_WRITE_CALLED)) { + data->state |= uWS::HttpResponseData::HTTP_WRITE_CALLED; + res->writeMark(); + } + } writeResponseHeader(res, name, value); } diff --git a/src/js/node/http.ts b/src/js/node/http.ts index e76c1155bdcd3..bb64936153379 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -1341,10 +1341,13 @@ const ServerResponsePrototype = { return this; } + if (!$isCallable(callback) && callback !== undefined) { + callback = undefined; + } + const handle = this[kHandle]; if (handle) { const headerState = this[headerStateSymbol]; - if (headerState !== NodeHTTPHeaderState.sent) { handle.cork(() => { handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]); @@ -1354,11 +1357,12 @@ const ServerResponsePrototype = { this[headerStateSymbol] = NodeHTTPHeaderState.sent; // https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/_http_outgoing.js#L987 - this._contentLength = handle.end(chunk, encoding, callback); + this._contentLength = handle.end(chunk, encoding); }); } else { - handle.end(chunk, encoding, callback); + handle.end(chunk, encoding); } + this.finished = true; } return this; @@ -1384,6 +1388,10 @@ const ServerResponsePrototype = { let result = 0; + if (!$isCallable(callback)) { + callback = undefined; + } + if (this[headerStateSymbol] !== NodeHTTPHeaderState.sent) { handle.cork(() => { handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]); @@ -1407,7 +1415,8 @@ const ServerResponsePrototype = { return false; } - this.emit("drain"); + if (result > 0) this.emit("drain"); + return true; }, From 68ad269ef0a46e32b339eaf1e3f678a1fb544d8f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 6 Oct 2024 21:34:49 -0700 Subject: [PATCH 03/36] more --- packages/bun-uws/src/HttpResponse.h | 39 ++-- packages/bun-uws/src/HttpResponseData.h | 4 +- src/bun.js/api/server.classes.ts | 15 ++ src/bun.js/api/server.zig | 262 ++++++++++++++++++++---- src/bun.js/bindings/NodeHTTP.cpp | 20 +- src/bun.js/bindings/ZigGlobalObject.cpp | 5 + src/bun.js/bindings/ZigGlobalObject.h | 4 +- src/bun.js/bindings/headers.h | 3 + src/deps/libuwsockets.cpp | 13 ++ src/deps/uws.zig | 16 ++ src/js/node/http.ts | 41 +++- src/tagged_pointer.zig | 6 +- 12 files changed, 350 insertions(+), 78 deletions(-) diff --git a/packages/bun-uws/src/HttpResponse.h b/packages/bun-uws/src/HttpResponse.h index 5cab0fa3d33d9..5c5f56cd85869 100644 --- a/packages/bun-uws/src/HttpResponse.h +++ b/packages/bun-uws/src/HttpResponse.h @@ -81,8 +81,12 @@ struct HttpResponse : public AsyncSocket { /* Called only once per request */ void writeMark() { + if (getHttpResponseData()->state & HttpResponseData::HTTP_WROTE_DATE_HEADER) { + return; + } /* Date is always written */ writeHeader("Date", std::string_view(((LoopData *) us_loop_ext(us_socket_context_loop(SSL, (us_socket_context(SSL, (us_socket_t *) this)))))->date, 29)); + getHttpResponseData()->state |= HttpResponseData::HTTP_WROTE_DATE_HEADER; } /* Returns true on success, indicating that it might be feasible to write more data. @@ -113,7 +117,8 @@ struct HttpResponse : public AsyncSocket { httpResponseData->state |= HttpResponseData::HTTP_CONNECTION_CLOSE; } - if (httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED) { + /* if write was called and there was previously no Content-Length header set */ + if (httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED && !(httpResponseData->state & HttpResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER)) { /* We do not have tryWrite-like functionalities, so ignore optional in this path */ @@ -152,7 +157,7 @@ struct HttpResponse : public AsyncSocket { return true; } else { /* Write content-length on first call */ - if (!(httpResponseData->state & HttpResponseData::HTTP_END_CALLED)) { + if (!(httpResponseData->state & (HttpResponseData::HTTP_END_CALLED))) { /* Write mark, this propagates to WebSockets too */ writeMark(); @@ -162,7 +167,8 @@ struct HttpResponse : public AsyncSocket { Super::write("Content-Length: ", 16); writeUnsigned64(totalSize); Super::write("\r\n\r\n", 4); - } else { + httpResponseData->state |= HttpResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER; + } else if (!(httpResponseData->state & (HttpResponseData::HTTP_WRITE_CALLED))) { Super::write("\r\n", 2); } @@ -427,7 +433,7 @@ struct HttpResponse : public AsyncSocket { /* End the response with an optional data chunk. Always starts a timeout. */ void end(std::string_view data = {}, bool closeConnection = false) { - internalEnd(data, data.length(), false, true, closeConnection); + internalEnd(data, data.length(), false, !(this->getHttpResponseData()->state & HttpResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER), closeConnection); } /* Try and end the response. Returns [true, true] on success. @@ -440,7 +446,7 @@ struct HttpResponse : public AsyncSocket { bool sendTerminatingChunk(bool closeConnection = false) { writeStatus(HTTP_200_OK); HttpResponseData *httpResponseData = getHttpResponseData(); - if (!(httpResponseData->state & (HttpResponseData::HTTP_WRITE_CALLED | HttpResponseData::HTTP_END_CALLED))) { + if (!(httpResponseData->state & (HttpResponseData::HTTP_WRITE_CALLED | HttpResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER))) { /* Write mark on first call to write */ writeMark(); @@ -469,21 +475,22 @@ struct HttpResponse : public AsyncSocket { HttpResponseData *httpResponseData = getHttpResponseData(); - if (!(httpResponseData->state & (HttpResponseData::HTTP_WRITE_CALLED | HttpResponseData::HTTP_END_CALLED))) { - /* Write mark on first call to write */ - writeMark(); + if (!(httpResponseData->state & HttpResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER)) { + if (!(httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED)) { + /* Write mark on first call to write */ + writeMark(); - writeHeader("Transfer-Encoding", "chunked"); - httpResponseData->state |= HttpResponseData::HTTP_WRITE_CALLED; - } + writeHeader("Transfer-Encoding", "chunked"); + httpResponseData->state |= HttpResponseData::HTTP_WRITE_CALLED; + } - - Super::write("\r\n", 2); - - // This happens if they call send, include a content-length header - if (!(httpResponseData->state & HttpResponseData::HTTP_END_CALLED)) { + Super::write("\r\n", 2); writeUnsignedHex((unsigned int) data.length()); Super::write("\r\n", 2); + } else if (!(httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED)) { + writeMark(); + Super::write("\r\n", 2); + httpResponseData->state |= HttpResponseData::HTTP_WRITE_CALLED; } auto [written, failed] = Super::write(data.data(), (int) data.length()); diff --git a/packages/bun-uws/src/HttpResponseData.h b/packages/bun-uws/src/HttpResponseData.h index 9613e84fe495a..3b07b7257bb1c 100644 --- a/packages/bun-uws/src/HttpResponseData.h +++ b/packages/bun-uws/src/HttpResponseData.h @@ -78,7 +78,9 @@ struct HttpResponseData : AsyncSocketData, HttpParser { HTTP_WRITE_CALLED = 2, // used HTTP_END_CALLED = 4, // used HTTP_RESPONSE_PENDING = 8, // used - HTTP_CONNECTION_CLOSE = 16 // used + HTTP_CONNECTION_CLOSE = 16, // used + HTTP_WROTE_CONTENT_LENGTH_HEADER = 32, // used + HTTP_WROTE_DATE_HEADER = 64, // used }; /* Shared context pointer */ diff --git a/src/bun.js/api/server.classes.ts b/src/bun.js/api/server.classes.ts index 27868539d6a41..d284e718d950e 100644 --- a/src/bun.js/api/server.classes.ts +++ b/src/bun.js/api/server.classes.ts @@ -120,6 +120,21 @@ export default [ fn: "abort", length: 0, }, + bufferedAmount: { + getter: "getBufferedAmount", + }, + aborted: { + getter: "getAborted", + }, + finished: { + getter: "getFinished", + }, + hasBody: { + getter: "getHasBody", + }, + ended: { + getter: "getEnded", + }, ondata: { getter: "getOnData", setter: "setOnData", diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 24d82309d5883..20554e6398adc 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -5809,12 +5809,15 @@ pub const NodeHTTPResponse = struct { finished: bool = false, ended: bool = false, has_body: bool = false, + promise: JSC.Strong = .{}, + server: AnyServer, const log = bun.Output.scoped(.NodeHTTPResponse, false); pub usingnamespace JSC.Codegen.JSNodeHTTPResponse; pub usingnamespace bun.NewRefCounted(@This(), deinit); pub fn create( + any_server_tag: u64, globalObject: *JSC.JSGlobalObject, has_body: *i32, request: *uws.Request, @@ -5835,6 +5838,7 @@ pub const NodeHTTPResponse = struct { } const response = NodeHTTPResponse.new(.{ + .server = AnyServer{ .ptr = AnyServer.Ptr.from(@ptrFromInt(any_server_tag)) }, .response = switch (is_ssl != 0) { true => uws.AnyResponse{ .SSL = @ptrCast(response_ptr) }, false => uws.AnyResponse{ .TCP = @ptrCast(response_ptr) }, @@ -5860,6 +5864,30 @@ pub const NodeHTTPResponse = struct { return this.finished or this.ended or this.aborted; } + pub fn getEnded(this: *const NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSValue.jsBoolean(this.ended); + } + + pub fn getFinished(this: *const NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSValue.jsBoolean(this.finished); + } + + pub fn getAborted(this: *const NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSValue.jsBoolean(this.aborted); + } + + pub fn getHasBody(this: *const NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSValue.jsBoolean(this.has_body); + } + + pub fn getBufferedAmount(this: *const NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + if (this.finished) { + return JSC.JSValue.jsNull(); + } + + return JSC.JSValue.jsNumber(this.response.getBufferedAmount()); + } + pub fn jsRef(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) JSC.JSValue { if (!this.isDone()) { this.js_ref.ref(globalObject.bunVM()); @@ -6021,11 +6049,58 @@ pub const NodeHTTPResponse = struct { log("onRequestComplete", .{}); this.finished = true; this.js_ref.unref(JSC.VirtualMachine.get()); + const server = this.server; this.clearJSValues(); this.deref(); + server.onRequestComplete(); + } + + pub export fn Bun__NodeHTTPRequest__onResolve(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + _ = globalObject; // autofix + log("onResolve", .{}); + const arguments = callframe.arguments(2).slice(); + const this: *NodeHTTPResponse = arguments[1].as(NodeHTTPResponse).?; + this.promise.deinit(); + defer this.deref(); + if (!this.finished and !this.aborted) { + this.clearJSValues(); + this.response.clearAborted(); + this.response.clearOnData(); + this.response.clearOnWritable(); + this.response.clearTimeout(); + this.response.endWithoutBody(this.response.state().isHttpConnectionClose()); + this.onRequestComplete(); + } + + return .undefined; + } + + pub export fn Bun__NodeHTTPRequest__onReject(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + const arguments = callframe.arguments(2).slice(); + const err = arguments[0]; + const this: *NodeHTTPResponse = arguments[1].as(NodeHTTPResponse).?; + this.promise.deinit(); + defer this.deref(); + + if (!this.finished and !this.aborted) { + this.clearJSValues(); + this.response.clearAborted(); + this.response.clearOnData(); + this.response.clearOnWritable(); + this.response.clearTimeout(); + if (!this.response.state().isHttpStatusCalled()) { + this.response.writeStatus("500 Internal Server Error"); + } + this.response.endStream(this.response.state().isHttpConnectionClose()); + this.onRequestComplete(); + } + + _ = globalObject.bunVM().uncaughtException(globalObject, err, true); + return .undefined; } fn clearJSValues(this: *NodeHTTPResponse) void { + // Promise is handled separately. this.onDataCallback.deinit(); this.strong_this.deinit(); this.onWritableCallback.deinit(); @@ -6062,6 +6137,7 @@ pub const NodeHTTPResponse = struct { if (last) { this.onDataCallback.deinit(); this.deref(); + this.deref(); } } const globalThis = this.onDataCallback.globalThis orelse JSC.VirtualMachine.get().global; @@ -6073,8 +6149,6 @@ pub const NodeHTTPResponse = struct { }); } else { this.response.clearOnData(); - this.onDataCallback.deinit(); - this.deref(); } } @@ -6166,24 +6240,20 @@ pub const NodeHTTPResponse = struct { const bytes = string_or_buffer.slice(); - const write_offset = this.response.getWriteOffset(); - if (is_end) { this.clearOnDataCallback(); - this.response.clearOnWritable(); this.response.clearAborted(); + this.response.clearOnWritable(); this.response.clearTimeout(); - } - if (is_end and bytes.len == 0 and write_offset == 0) { - this.response.endStream(state.isHttpConnectionClose()); + this.ended = true; + if (!state.isHttpWriteCalled()) { + this.response.end(bytes, state.isHttpConnectionClose()); + } else { + this.response.endStream(state.isHttpConnectionClose()); + } this.onRequestComplete(); - return JSC.JSValue.jsNumberFromInt32(0); - } - if (is_end) { - this.response.end(bytes, state.isHttpConnectionClose()); - this.onRequestComplete(); return JSC.JSValue.jsNumberFromUint64(bytes.len); } else { switch (this.response.write(bytes)) { @@ -6314,6 +6384,7 @@ pub const NodeHTTPResponse = struct { this.onDataCallback.deinit(); this.onWritableCallback.deinit(); this.strong_this.deinit(); + this.promise.deinit(); this.destroy(); } @@ -7411,31 +7482,120 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp req.setYield(false); resp.timeout(this.config.idleTimeout); + const globalThis = this.globalThis; + const thisObject = this.thisObject; + const vm = this.vm; + var node_http_response: ?*NodeHTTPResponse = null; + var is_async = false; defer { - if (node_http_response) |node_response| { - node_response.deref(); + if (!is_async) { + if (node_http_response) |node_response| { + node_response.deref(); + } } } - const result = onNodeHTTPRequestFn( - this.globalThis, - this.thisObject, + const result: JSValue = onNodeHTTPRequestFn( + @bitCast(AnyServer.from(this)), + globalThis, + thisObject, this.config.onNodeHTTPRequest, req, resp, &node_http_response, ); - if (result.toError()) |err| { - _ = this.vm.uncaughtException(this.globalThis, err, false); - return; + const HTTPResult = union(enum) { + rejection: JSC.JSValue, + exception: JSC.JSValue, + success: void, + pending: JSC.JSValue, + }; + var strong_promise: JSC.Strong = .{}; + var needs_to_drain = true; + + defer { + if (needs_to_drain) { + vm.drainMicrotasks(); + } } + defer strong_promise.deinit(); + const http_result: HTTPResult = brk: { + if (result.toError()) |err| { + break :brk .{ .exception = err }; + } - this.vm.drainMicrotasks(); + if (result.asAnyPromise()) |promise| { + if (promise.status(globalThis.vm()) == .pending) { + strong_promise.set(globalThis, result); + needs_to_drain = false; + vm.drainMicrotasks(); + } + + switch (promise.status(globalThis.vm())) { + .fulfilled => { + globalThis.handleRejectedPromises(); + break :brk .{ .success = {} }; + }, + .rejected => { + promise.setHandled(globalThis.vm()); + break :brk .{ .rejection = promise.result(globalThis.vm()) }; + }, + .pending => { + globalThis.handleRejectedPromises(); + if (node_http_response) |node_response| { + if (node_response.finished or node_response.aborted) { + strong_promise.deinit(); + break :brk .{ .success = {} }; + } + + if (node_response.strong_this.get()) |strong_self| { + node_response.promise = strong_promise; + strong_promise = .{}; + result._then(globalThis, strong_self, NodeHTTPResponse.Bun__NodeHTTPRequest__onResolve, NodeHTTPResponse.Bun__NodeHTTPRequest__onReject); + } else { + @panic("This should not happen"); + } + + is_async = true; + } + + break :brk .{ .pending = result }; + }, + } + } + + break :brk .{ .success = {} }; + }; + + switch (http_result) { + .exception, .rejection => |err| { + _ = vm.uncaughtException(globalThis, err, http_result == .rejection); + + if (node_http_response) |node_response| { + if (!node_response.finished and node_response.response.state().isResponsePending()) { + node_response.clearJSValues(); + node_response.response.clearAborted(); + node_response.response.clearOnData(); + node_response.response.clearTimeout(); + node_response.response.clearOnWritable(); + node_response.finished = true; + if (node_response.response.state().isHttpStatusCalled()) { + node_response.response.writeStatus("500 Internal Server Error"); + node_response.response.endWithoutBody(true); + } else { + node_response.response.endStream(true); + } + } + } + }, + .success => {}, + .pending => {}, + } if (node_http_response) |node_response| { - if (!node_response.isDone()) { + if (!node_response.finished and node_response.response.state().isResponsePending()) { node_response.setOnAbortedHandler(); } } @@ -7791,43 +7951,57 @@ pub const HTTPServer = NewServer(JSC.Codegen.JSHTTPServer, false, false); pub const HTTPSServer = NewServer(JSC.Codegen.JSHTTPSServer, true, false); pub const DebugHTTPServer = NewServer(JSC.Codegen.JSDebugHTTPServer, false, true); pub const DebugHTTPSServer = NewServer(JSC.Codegen.JSDebugHTTPSServer, true, true); -const AnyServer = union(enum) { - HTTPServer: *HTTPServer, - HTTPSServer: *HTTPSServer, - DebugHTTPServer: *DebugHTTPServer, - DebugHTTPSServer: *DebugHTTPSServer, +const AnyServer = packed struct { + ptr: Ptr, + + const Ptr = bun.TaggedPointerUnion(.{ + HTTPServer, + HTTPSServer, + DebugHTTPServer, + DebugHTTPSServer, + }); pub fn config(this: AnyServer) *const ServerConfig { - return switch (this) { - inline else => |server| &server.config, + return switch (this.ptr.tag()) { + Ptr.case(HTTPServer) => &this.ptr.as(HTTPServer).config, + Ptr.case(HTTPSServer) => &this.ptr.as(HTTPSServer).config, + Ptr.case(DebugHTTPServer) => &this.ptr.as(DebugHTTPServer).config, + Ptr.case(DebugHTTPSServer) => &this.ptr.as(DebugHTTPSServer).config, + else => bun.unreachablePanic("Invalid pointer tag", .{}), }; } pub fn from(server: anytype) AnyServer { - return switch (@TypeOf(server)) { - *HTTPServer => AnyServer{ .HTTPServer = server }, - *HTTPSServer => AnyServer{ .HTTPSServer = server }, - *DebugHTTPServer => AnyServer{ .DebugHTTPServer = server }, - *DebugHTTPSServer => AnyServer{ .DebugHTTPSServer = server }, - else => @compileError("Invalid server type"), - }; + return .{ .ptr = Ptr.init(server) }; } pub fn onPendingRequest(this: AnyServer) void { - switch (this) { - inline else => |server| server.onPendingRequest(), + switch (this.ptr.tag()) { + Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onPendingRequest(), + Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onPendingRequest(), + Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onPendingRequest(), + Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onPendingRequest(), + else => bun.unreachablePanic("Invalid pointer tag", .{}), } } pub fn onRequestComplete(this: AnyServer) void { - switch (this) { - inline else => |server| server.onRequestComplete(), + switch (this.ptr.tag()) { + Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onRequestComplete(), + Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onRequestComplete(), + Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onRequestComplete(), + Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onRequestComplete(), + else => bun.unreachablePanic("Invalid pointer tag", .{}), } } pub fn onStaticRequestComplete(this: AnyServer) void { - switch (this) { - inline else => |server| server.onStaticRequestComplete(), + switch (this.ptr.tag()) { + Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onStaticRequestComplete(), + Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onStaticRequestComplete(), + Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onStaticRequestComplete(), + Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onStaticRequestComplete(), + else => bun.unreachablePanic("Invalid pointer tag", .{}), } } }; @@ -7873,6 +8047,7 @@ comptime { } extern fn NodeHTTPServer__onRequest_http( + any_server: u64, globalThis: *JSC.JSGlobalObject, this: JSC.JSValue, callback: JSC.JSValue, @@ -7882,6 +8057,7 @@ extern fn NodeHTTPServer__onRequest_http( ) JSC.JSValue; extern fn NodeHTTPServer__onRequest_https( + any_server: u64, globalThis: *JSC.JSGlobalObject, this: JSC.JSValue, callback: JSC.JSValue, diff --git a/src/bun.js/bindings/NodeHTTP.cpp b/src/bun.js/bindings/NodeHTTP.cpp index 7ce532ea42f34..2554874a9cb98 100644 --- a/src/bun.js/bindings/NodeHTTP.cpp +++ b/src/bun.js/bindings/NodeHTTP.cpp @@ -415,10 +415,11 @@ static EncodedJSValue assignHeadersFromUWebSockets(uWS::HttpRequest* request, JS return JSValue::encode(tuple); } -extern "C" EncodedJSValue NodeHTTPResponse__createForJS(JSC::JSGlobalObject* globalObject, int* hasBody, uWS::HttpRequest* request, int isSSL, void* response_ptr, void** nodeHttpResponsePtr); +extern "C" EncodedJSValue NodeHTTPResponse__createForJS(size_t any_server, JSC::JSGlobalObject* globalObject, int* hasBody, uWS::HttpRequest* request, int isSSL, void* response_ptr, void** nodeHttpResponsePtr); template static EncodedJSValue NodeHTTPServer__onRequest( + size_t any_server, JSC::JSGlobalObject* globalObject, JSValue thisValue, JSValue callback, @@ -442,7 +443,7 @@ static EncodedJSValue NodeHTTPServer__onRequest( } int hasBody = 0; - EncodedJSValue nodehttpobjectValue = NodeHTTPResponse__createForJS(globalObject, &hasBody, request, isSSL, response, nodeHttpResponsePtr); + EncodedJSValue nodehttpobjectValue = NodeHTTPResponse__createForJS(any_server, globalObject, &hasBody, request, isSSL, response, nodeHttpResponsePtr); JSC::CallData callData = getCallData(callbackObject); args.append(JSValue::decode(nodehttpobjectValue)); @@ -530,13 +531,8 @@ static void writeFetchHeadersToUWSResponse(WebCore::FetchHeaders& headers, uWS:: // < // if (header.key == WebCore::HTTPHeaderName::ContentLength) { - if (!(data->state & uWS::HttpResponseData::HTTP_END_CALLED)) { - data->state |= uWS::HttpResponseData::HTTP_END_CALLED; - res->writeMark(); - } - } else if (header.key == WebCore::HTTPHeaderName::TransferEncoding || header.key == WebCore::HTTPHeaderName::Date) { - if (!(data->state & uWS::HttpResponseData::HTTP_WRITE_CALLED)) { - data->state |= uWS::HttpResponseData::HTTP_WRITE_CALLED; + if (!(data->state & uWS::HttpResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER)) { + data->state |= uWS::HttpResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER; res->writeMark(); } } @@ -639,6 +635,7 @@ extern "C" void NodeHTTPServer__writeHead_https( } extern "C" EncodedJSValue NodeHTTPServer__onRequest_http( + size_t any_server, JSC::JSGlobalObject* globalObject, EncodedJSValue thisValue, EncodedJSValue callback, @@ -646,10 +643,11 @@ extern "C" EncodedJSValue NodeHTTPServer__onRequest_http( uWS::HttpResponse* response, void** nodeHttpResponsePtr) { - return NodeHTTPServer__onRequest(globalObject, JSValue::decode(thisValue), JSValue::decode(callback), request, response, nodeHttpResponsePtr); + return NodeHTTPServer__onRequest(any_server, globalObject, JSValue::decode(thisValue), JSValue::decode(callback), request, response, nodeHttpResponsePtr); } extern "C" EncodedJSValue NodeHTTPServer__onRequest_https( + size_t any_server, JSC::JSGlobalObject* globalObject, EncodedJSValue thisValue, EncodedJSValue callback, @@ -657,7 +655,7 @@ extern "C" EncodedJSValue NodeHTTPServer__onRequest_https( uWS::HttpResponse* response, void** nodeHttpResponsePtr) { - return NodeHTTPServer__onRequest(globalObject, JSValue::decode(thisValue), JSValue::decode(callback), request, response, nodeHttpResponsePtr); + return NodeHTTPServer__onRequest(any_server, globalObject, JSValue::decode(thisValue), JSValue::decode(callback), request, response, nodeHttpResponsePtr); } JSC_DEFINE_HOST_FUNCTION(jsHTTPAssignHeaders, (JSGlobalObject * globalObject, CallFrame* callFrame)) diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 7da7d3e3badac..f8466406ea330 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -4126,6 +4126,7 @@ JSC::JSValue EvalGlobalObject::moduleLoaderEvaluate(JSGlobalObject* lexicalGloba GlobalObject::PromiseFunctions GlobalObject::promiseHandlerID(Zig::FFIFunction handler) { + if (handler == Bun__HTTPRequestContext__onReject) { return GlobalObject::PromiseFunctions::Bun__HTTPRequestContext__onReject; } else if (handler == Bun__HTTPRequestContext__onRejectStream) { @@ -4178,6 +4179,10 @@ GlobalObject::PromiseFunctions GlobalObject::promiseHandlerID(Zig::FFIFunction h return GlobalObject::PromiseFunctions::Bun__onResolveEntryPointResult; } else if (handler == Bun__onRejectEntryPointResult) { return GlobalObject::PromiseFunctions::Bun__onRejectEntryPointResult; + } else if (handler == Bun__NodeHTTPRequest__onResolve) { + return GlobalObject::PromiseFunctions::Bun__NodeHTTPRequest__onResolve; + } else if (handler == Bun__NodeHTTPRequest__onReject) { + return GlobalObject::PromiseFunctions::Bun__NodeHTTPRequest__onReject; } else { RELEASE_ASSERT_NOT_REACHED(); } diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 52226012dea72..b80d29315baa7 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -354,8 +354,10 @@ class GlobalObject : public Bun::GlobalScope { Bun__BodyValueBufferer__onResolveStream, Bun__onResolveEntryPointResult, Bun__onRejectEntryPointResult, + Bun__NodeHTTPRequest__onResolve, + Bun__NodeHTTPRequest__onReject, }; - static constexpr size_t promiseFunctionsSize = 24; + static constexpr size_t promiseFunctionsSize = 25; static PromiseFunctions promiseHandlerID(SYSV_ABI EncodedJSValue (*handler)(JSC__JSGlobalObject* arg0, JSC__CallFrame* arg1)); diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index 6d5efdb701743..4348468805304 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -790,6 +790,9 @@ BUN_DECLARE_HOST_FUNCTION(Bun__HTTPRequestContext__onRejectStream); BUN_DECLARE_HOST_FUNCTION(Bun__HTTPRequestContext__onResolve); BUN_DECLARE_HOST_FUNCTION(Bun__HTTPRequestContext__onResolveStream); +BUN_DECLARE_HOST_FUNCTION(Bun__NodeHTTPRequest__onResolve); +BUN_DECLARE_HOST_FUNCTION(Bun__NodeHTTPRequest__onReject); + #endif #ifdef __cplusplus diff --git a/src/deps/libuwsockets.cpp b/src/deps/libuwsockets.cpp index a4af0b0673fb7..69da92812deed 100644 --- a/src/deps/libuwsockets.cpp +++ b/src/deps/libuwsockets.cpp @@ -275,6 +275,19 @@ extern "C" } } + size_t uws_res_get_buffered_amount(int ssl, uws_res_t *res) nonnull_fn_decl; + + size_t uws_res_get_buffered_amount(int ssl, uws_res_t *res) + { + if (ssl) { + uWS::HttpResponse *uwsRes = (uWS::HttpResponse *)res; + return uwsRes->getBufferedAmount(); + } else { + uWS::HttpResponse *uwsRes = (uWS::HttpResponse *)res; + return uwsRes->getBufferedAmount(); + } + } + void uws_app_any(int ssl, uws_app_t *app, const char *pattern_ptr, size_t pattern_len, uws_method_handler handler, void *user_data) { std::string pattern = std::string(pattern_ptr, pattern_len); diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 6d554ae41f4de..f15f8d651dc50 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -3047,6 +3047,13 @@ pub const AnyResponse = union(enum) { }; } + pub fn getBufferedAmount(this: AnyResponse) u64 { + return switch (this) { + .SSL => |resp| resp.getBufferedAmount(), + .TCP => |resp| resp.getBufferedAmount(), + }; + } + pub fn writeContinue(this: AnyResponse) void { return switch (this) { .SSL => |resp| resp.writeContinue(), @@ -3561,6 +3568,9 @@ pub fn NewApp(comptime ssl: bool) type { pub fn resetTimeout(res: *Response) void { uws_res_reset_timeout(ssl_flag, res.downcast()); } + pub fn getBufferedAmount(res: *Response) u64 { + return uws_res_get_buffered_amount(ssl_flag, res.downcast()); + } pub fn write(res: *Response, data: []const u8) WriteResult { var len: usize = data.len; return switch (uws_res_write(ssl_flag, res.downcast(), data.ptr, &len)) { @@ -3991,6 +4001,7 @@ extern fn uws_res_end_without_body(ssl: i32, res: *uws_res, close_connection: bo extern fn uws_res_end_sendfile(ssl: i32, res: *uws_res, write_offset: u64, close_connection: bool) void; extern fn uws_res_timeout(ssl: i32, res: *uws_res, timeout: u8) void; extern fn uws_res_reset_timeout(ssl: i32, res: *uws_res) void; +extern fn uws_res_get_buffered_amount(ssl: i32, res: *uws_res) u64; extern fn uws_res_write(ssl: i32, res: *uws_res, data: ?[*]const u8, length: *usize) bool; extern fn uws_res_get_write_offset(ssl: i32, res: *uws_res) u64; extern fn uws_res_override_write_offset(ssl: i32, res: *uws_res, u64) void; @@ -4088,6 +4099,7 @@ pub const State = enum(u8) { HTTP_END_CALLED = 4, HTTP_RESPONSE_PENDING = 8, HTTP_CONNECTION_CLOSE = 16, + HTTP_WROTE_CONTENT_LENGTH_HEADER = 32, _, @@ -4095,6 +4107,10 @@ pub const State = enum(u8) { return @intFromEnum(this) & @intFromEnum(State.HTTP_RESPONSE_PENDING) != 0; } + pub inline fn hasWrittenContentLengthHeader(this: State) bool { + return @intFromEnum(this) & @intFromEnum(State.HTTP_WROTE_CONTENT_LENGTH_HEADER) != 0; + } + pub inline fn isHttpEndCalled(this: State) bool { return @intFromEnum(this) & @intFromEnum(State.HTTP_END_CALLED) != 0; } diff --git a/src/js/node/http.ts b/src/js/node/http.ts index bb64936153379..a454bffc29575 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -669,7 +669,42 @@ Server.prototype = { [kHandle]: handle, }); + let capturedError; + let rejectFunction; + let errorCallback = err => { + if (capturedError) return; + capturedError = err; + if (rejectFunction) rejectFunction(err); + }; + + let resolveFunction; + let didFinish = false; + let closeCallback = () => { + didFinish = true; + if (resolveFunction) resolveFunction(http_res); + }; + + http_req.once("error", errorCallback); + http_res.once("error", errorCallback); + http_res.once("close", closeCallback); + server.emit("request", http_req, http_res); + + if (capturedError) { + throw capturedError; + } + + if (handle.finished || didFinish) { + http_res.off("close", closeCallback); + closeCallback = () => {}; + return; + } + + const { reject, resolve, promise } = $newPromiseCapability(Promise); + resolveFunction = resolve; + rejectFunction = reject; + + return promise; }, // Be very careful not to access (web) Request object @@ -916,7 +951,7 @@ IncomingMessage.prototype = { this.push(null); } else if ((internalRequest = this[kInternalRequest])) { internalRequest.ondata = (chunk, isLast, isAborted) => { - console.log("ondata", chunk, isLast, isAborted); + $debug("ondata", chunk, isLast, isAborted); if (isAborted) { if (!this.destroyed) { this.destroy(); @@ -1214,10 +1249,6 @@ const OutgoingMessagePrototype = { _send(data, encoding, callback, byteLength) { return this.write(data, encoding, callback); }, - - [EventEmitter.captureRejectionSymbol]: function (err, event) { - this.destroy(err); - }, } satisfies typeof import("node:http").OutgoingMessage.prototype; OutgoingMessage.prototype = OutgoingMessagePrototype; $setPrototypeDirect.$call(OutgoingMessage, Writable); diff --git a/src/tagged_pointer.zig b/src/tagged_pointer.zig index ef9122582ff0f..9cbbeb7993c7e 100644 --- a/src/tagged_pointer.zig +++ b/src/tagged_pointer.zig @@ -100,7 +100,7 @@ pub fn TaggedPointerUnion(comptime Types: anytype) type { const TagType: type = result.tag_type; - return struct { + return packed struct { pub const Tag = TagType; pub const TagInt = TagSize; pub const type_map: TypeMap(Types) = result.ty_map; @@ -147,6 +147,10 @@ pub fn TaggedPointerUnion(comptime Types: anytype) type { return @as(TagType, @enumFromInt(this.repr.data)); } + pub fn case(comptime Type: type) Tag { + return @field(Tag, typeBaseName(@typeName(Type))); + } + /// unsafely cast a tagged pointer to a specific type, without checking that it's really that type pub inline fn as(this: This, comptime Type: type) *Type { comptime assert_type(Type); From 432e92fd27c32a5a385b5f7ad460541c1c6f5f5d Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 6 Oct 2024 22:21:47 -0700 Subject: [PATCH 04/36] Update dev-server.test.ts --- test/integration/next-pages/test/dev-server.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/next-pages/test/dev-server.test.ts b/test/integration/next-pages/test/dev-server.test.ts index ae7fadb38dcb6..6a3c553c6e15e 100644 --- a/test/integration/next-pages/test/dev-server.test.ts +++ b/test/integration/next-pages/test/dev-server.test.ts @@ -79,7 +79,7 @@ async function getDevServerURL() { readStream() .catch(e => reject(e)) .finally(() => { - dev_server.unref?.(); + dev_server?.unref?.(); }); await promise; return baseUrl; From 7b295c0d187905259b522aaef8c9d954c8bcf07c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 7 Oct 2024 06:52:22 -0700 Subject: [PATCH 05/36] more --- src/bun.js/api/server.zig | 262 ++++++-- src/bun.js/bindings/NodeHTTP.cpp | 14 + src/bun.js/webcore/body.zig | 14 + src/bun.js/webcore/request.zig | 6 +- src/bun.js/webcore/response.zig | 52 ++ src/codegen/client-js.ts | 2 +- src/deps/uws.zig | 23 + src/js/node/http.ts | 644 +++++++++++++++---- test/js/node/http/fixtures/log-events.mjs | 7 +- test/js/node/http/node-http.test.ts | 49 +- test/js/web/fetch/fetch.stream.test.ts | 2 +- test/regression/issue/04298/node-fixture.mjs | 41 ++ 12 files changed, 875 insertions(+), 241 deletions(-) create mode 100644 test/regression/issue/04298/node-fixture.mjs diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 20554e6398adc..7dbb7f172b871 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -5808,14 +5808,21 @@ pub const NodeHTTPResponse = struct { aborted: bool = false, finished: bool = false, ended: bool = false, - has_body: bool = false, + body_read_state: BodyReadState = .none, + body_read_ref: JSC.Ref = .{}, promise: JSC.Strong = .{}, server: AnyServer, const log = bun.Output.scoped(.NodeHTTPResponse, false); - pub usingnamespace JSC.Codegen.JSNodeHTTPResponse; pub usingnamespace bun.NewRefCounted(@This(), deinit); + + pub const BodyReadState = enum { + none, + pending, + done, + }; + pub fn create( any_server_tag: u64, globalObject: *JSC.JSGlobalObject, @@ -5825,6 +5832,7 @@ pub const NodeHTTPResponse = struct { response_ptr: *anyopaque, node_response_ptr: *?*NodeHTTPResponse, ) callconv(.C) JSC.JSValue { + const vm = globalObject.bunVM(); if ((HTTP.Method.which(request.method()) orelse HTTP.Method.OPTIONS).hasRequestBody()) { const req_len: usize = brk: { if (request.header("content-length")) |content_length| { @@ -5843,13 +5851,17 @@ pub const NodeHTTPResponse = struct { true => uws.AnyResponse{ .SSL = @ptrCast(response_ptr) }, false => uws.AnyResponse{ .TCP = @ptrCast(response_ptr) }, }, - .has_body = has_body.* != 0, - // 1 - the HTTP request + .body_read_state = if (has_body.* != 0) .pending else .none, + // 1 - the HTTP response // 1 - the JS object // 1 - the Server handler. - .ref_count = 3, + // 1 - the onData callback (request bod) + .ref_count = if (has_body.* != 0) 4 else 3, }); - response.js_ref.ref(globalObject.bunVM()); + if (has_body.* != 0) { + response.body_read_ref.ref(vm); + } + response.js_ref.ref(vm); const js_this = response.toJS(globalObject); response.strong_this.set(globalObject, js_this); node_response_ptr.* = response; @@ -5858,6 +5870,7 @@ pub const NodeHTTPResponse = struct { pub fn setOnAbortedHandler(this: *NodeHTTPResponse) void { this.response.onAborted(*NodeHTTPResponse, onAbort, this); + this.response.onTimeout(*NodeHTTPResponse, onTimeout, this); } fn isDone(this: *const NodeHTTPResponse) bool { @@ -5877,7 +5890,7 @@ pub const NodeHTTPResponse = struct { } pub fn getHasBody(this: *const NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { - return JSC.JSValue.jsBoolean(this.has_body); + return JSC.JSValue.jsBoolean(this.body_read_state != .none); } pub fn getBufferedAmount(this: *const NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { @@ -5944,8 +5957,8 @@ pub const NodeHTTPResponse = struct { } const status_code_value = if (arguments.len > 0) arguments[0] else .undefined; - const status_message_value = if (arguments.len > 1) arguments[1] else .undefined; - const headers_object_value = if (arguments.len > 2) arguments[2] else .undefined; + const status_message_value = if (arguments.len > 1 and arguments[1] != .null) arguments[1] else .undefined; + const headers_object_value = if (arguments.len > 2 and arguments[2] != .null) arguments[2] else .undefined; const status_code: i32 = brk: { if (status_code_value != .undefined) { @@ -5958,11 +5971,14 @@ pub const NodeHTTPResponse = struct { break :brk 200; }; + var stack_fallback = std.heap.stackFallback(256, bun.default_allocator); + const allocator = stack_fallback.get(); const status_message_slice = if (status_message_value != .undefined) - status_message_value.toSlice(globalObject, bun.default_allocator) + status_message_value.toSlice(globalObject, allocator) else ZigString.Slice.empty; defer status_message_slice.deinit(); + if (globalObject.hasException()) { return .zero; } @@ -5976,8 +5992,8 @@ pub const NodeHTTPResponse = struct { } const message = if (status_message_slice.len > 0) status_message_slice.slice() else "HM"; - const status_message = std.fmt.allocPrint(bun.default_allocator, "{d} {s}", .{ status_code, message }) catch bun.outOfMemory(); - defer bun.default_allocator.free(status_message); + const status_message = std.fmt.allocPrint(allocator, "{d} {s}", .{ status_code, message }) catch bun.outOfMemory(); + defer allocator.free(status_message); writeHeadInternal(this.response, globalObject, status_message, headers_object_value); break :do_it; } @@ -6009,39 +6025,63 @@ pub const NodeHTTPResponse = struct { return .undefined; } - pub fn onAbort(this: *NodeHTTPResponse, resp: uws.AnyResponse) void { + pub const AbortEvent = enum(u8) { + none = 0, + abort = 1, + timeout = 2, + }; + + fn handleAbortOrTimeout(this: *NodeHTTPResponse, resp: uws.AnyResponse, comptime event: AbortEvent) void { _ = resp; // autofix if (this.finished) { return; } - defer this.onRequestComplete(); - this.aborted = true; - log("onAbort", .{}); + + defer if (event == .abort) this.onRequestComplete(); + if (event == .abort) { + this.aborted = true; + } this.ref(); defer this.deref(); - const js_this = this.strong_this.trySwap() orelse .undefined; + const js_this: JSValue = brk: { + if (comptime event == .abort) { + break :brk this.strong_this.trySwap() orelse .undefined; + } + break :brk this.strong_this.get() orelse .undefined; + }; - if (this.onAbortedCallback.trySwap()) |on_aborted| { + if (this.onAbortedCallback.get()) |on_aborted| { + defer { + if (event == .abort) { + this.onAbortedCallback.deinit(); + } + } const globalThis = this.onAbortedCallback.globalThis orelse JSC.VirtualMachine.get().global; const vm = globalThis.bunVM(); const event_loop = vm.eventLoop(); event_loop.runCallback(on_aborted, globalThis, js_this, &.{ - js_this, + JSC.JSValue.jsNumber(@intFromEnum(event)), }); } - if (this.onDataCallback.trySwap()) |onDataCallback| { - defer this.deref(); - const globalThis = this.onDataCallback.globalThis orelse JSC.VirtualMachine.get().global; - const vm = globalThis.bunVM(); - const event_loop = vm.eventLoop(); - event_loop.runCallback(onDataCallback, globalThis, .undefined, &.{ .undefined, JSC.jsBoolean(true), JSC.jsBoolean(true) }); + if (event == .abort) { + this.onDataOrAborted("", true, .abort); } } + pub fn onAbort(this: *NodeHTTPResponse, response: uws.AnyResponse) void { + log("onAbort", .{}); + this.handleAbortOrTimeout(response, .abort); + } + + pub fn onTimeout(this: *NodeHTTPResponse, response: uws.AnyResponse) void { + log("onTimeout", .{}); + this.handleAbortOrTimeout(response, .timeout); + } + fn onRequestComplete(this: *NodeHTTPResponse) void { if (this.finished) { return; @@ -6049,10 +6089,13 @@ pub const NodeHTTPResponse = struct { log("onRequestComplete", .{}); this.finished = true; this.js_ref.unref(JSC.VirtualMachine.get()); + const server = this.server; this.clearJSValues(); + if (this.body_read_state != .pending) { + server.onRequestComplete(); + } this.deref(); - server.onRequestComplete(); } pub export fn Bun__NodeHTTPRequest__onResolve(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { @@ -6101,7 +6144,6 @@ pub const NodeHTTPResponse = struct { fn clearJSValues(this: *NodeHTTPResponse) void { // Promise is handled separately. - this.onDataCallback.deinit(); this.strong_this.deinit(); this.onWritableCallback.deinit(); this.onAbortedCallback.deinit(); @@ -6129,28 +6171,41 @@ pub const NodeHTTPResponse = struct { return .undefined; } - pub fn onData(this: *NodeHTTPResponse, chunk: []const u8, last: bool) void { - log("onData({d} bytes, is_last = {d})", .{ chunk.len, @intFromBool(last) }); - if (this.onDataCallback.get()) |callback| { - if (last) this.ref(); - defer { - if (last) { - this.onDataCallback.deinit(); - this.deref(); + fn onDataOrAborted(this: *NodeHTTPResponse, chunk: []const u8, last: bool, event: AbortEvent) void { + if (last) { + this.ref(); + this.body_read_state = .done; + } + + defer { + if (last) { + if (this.body_read_ref.has) { + this.body_read_ref.unref(JSC.VirtualMachine.get()); + if (this.finished) { + this.server.onRequestComplete(); + } this.deref(); } + + this.deref(); } + } + + if (this.onDataCallback.get()) |callback| { const globalThis = this.onDataCallback.globalThis orelse JSC.VirtualMachine.get().global; const event_loop = globalThis.bunVM().eventLoop(); event_loop.runCallback(callback, globalThis, .undefined, &.{ - JSC.ArrayBuffer.createBuffer(globalThis, chunk), + if (chunk.len > 0) JSC.ArrayBuffer.createBuffer(globalThis, chunk) else .undefined, JSC.JSValue.jsBoolean(last), - JSC.JSValue.jsBoolean(false), + JSC.JSValue.jsNumber(@intFromEnum(event)), }); - } else { - this.response.clearOnData(); } } + pub fn onData(this: *NodeHTTPResponse, chunk: []const u8, last: bool) void { + log("onData({d} bytes, is_last = {d})", .{ chunk.len, @intFromBool(last) }); + + onDataOrAborted(this, chunk, last, .none); + } fn onDrain(this: *NodeHTTPResponse, offset: u64, response: uws.AnyResponse) bool { log("onDrain({d})", .{offset}); @@ -6241,13 +6296,11 @@ pub const NodeHTTPResponse = struct { const bytes = string_or_buffer.slice(); if (is_end) { - this.clearOnDataCallback(); this.response.clearAborted(); this.response.clearOnWritable(); this.response.clearTimeout(); - this.ended = true; - if (!state.isHttpWriteCalled()) { + if (!state.isHttpWriteCalled() or bytes.len > 0) { this.response.end(bytes, state.isHttpConnectionClose()); } else { this.response.endStream(state.isHttpConnectionClose()); @@ -6306,24 +6359,45 @@ pub const NodeHTTPResponse = struct { } fn clearOnDataCallback(this: *NodeHTTPResponse) void { - if (this.onDataCallback.has()) { + if (this.body_read_state != .none) { this.onDataCallback.deinit(); this.response.clearOnData(); - this.deref(); + if (this.body_read_state != .done) { + this.body_read_state = .done; + this.deref(); + } } } pub fn setOnData(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, value: JSValue) bool { - if (!this.has_body or this.isDone() or value == .undefined) { - this.clearOnDataCallback(); + if (value == .undefined or this.ended or this.aborted or this.body_read_state == .none) { + this.onDataCallback.deinit(); + defer { + if (this.body_read_ref.has) { + this.body_read_ref.unref(globalObject.bunVM()); + this.deref(); + } + } + switch (this.body_read_state) { + .pending, .done => { + if (!this.ended and !this.aborted) { + this.response.clearOnData(); + } + this.body_read_state = .done; + }, + .none => {}, + } return true; } - if (!this.onDataCallback.has()) { - this.ref(); - } this.onDataCallback.set(globalObject, value.withAsyncContextIfNeeded(globalObject)); this.response.onData(*NodeHTTPResponse, onData, this); + + if (!this.body_read_ref.has) { + this.ref(); + this.body_read_ref.ref(globalObject.bunVM()); + } + return true; } @@ -6345,12 +6419,31 @@ pub const NodeHTTPResponse = struct { return; }; } + + export fn NodeHTTPResponse__setTimeout(this: *NodeHTTPResponse, seconds: JSC.JSValue, globalThis: *JSC.JSGlobalObject) void { + if (!seconds.isNumber()) { + _ = globalThis.throwInvalidArgumentTypeValue("timeout", "number", seconds); + return; + } + + if (this.finished or this.aborted) { + return; + } + + this.response.timeout(@intCast(@min(seconds.to(c_uint), 255))); + } + pub fn cork(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { const arguments = callframe.arguments(1).slice(); if (arguments.len == 0 or !arguments[0].isCallable(globalObject.vm())) { return globalObject.throwInvalidArgumentTypeValue("cork", "function", arguments[0]); } + if (this.finished or this.aborted) { + globalObject.ERR_STREAM_ALREADY_FINISHED("Stream is already ended", .{}).throw(); + return .zero; + } + var result: JSC.JSValue = .zero; var is_exception: bool = false; this.ref(); @@ -6380,6 +6473,7 @@ pub const NodeHTTPResponse = struct { pub fn deinit(this: *NodeHTTPResponse) void { this.js_ref.unref(JSC.VirtualMachine.get()); + this.body_read_ref.unref(JSC.VirtualMachine.get()); this.onAbortedCallback.deinit(); this.onDataCallback.deinit(); this.onWritableCallback.deinit(); @@ -6435,8 +6529,8 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp pub const doPublish = JSC.wrapInstanceMethod(ThisServer, "publish", false); pub const doReload = onReload; pub const doFetch = onFetch; - pub const doRequestIP = JSC.wrapInstanceMethod(ThisServer, "requestIP", false); - pub const doTimeout = JSC.wrapInstanceMethod(ThisServer, "timeout", false); + pub const doRequestIP = requestIP; + pub const doTimeout = timeout; pub fn doSubscriberCount(this: *ThisServer, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { const arguments = callframe.arguments(1); @@ -6473,28 +6567,70 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp extern fn JSSocketAddress__create(global: *JSC.JSGlobalObject, ip: JSValue, port: i32, is_ipv6: bool) JSValue; - pub fn requestIP(this: *ThisServer, request: *JSC.WebCore.Request) JSC.JSValue { + pub fn requestIP(this: *ThisServer, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + const arguments = callframe.arguments(1).slice(); + if (arguments.len < 1 or arguments[0].isEmptyOrUndefinedOrNull()) { + globalObject.throwNotEnoughArguments("requestIP", 1, 0); + return .zero; + } + if (this.config.address == .unix) { return JSValue.jsNull(); } - return if (request.request_context.getRemoteSocketInfo()) |info| - JSSocketAddress__create( - this.globalThis, - bun.String.init(info.ip).toJS(this.globalThis), - info.port, - info.is_ipv6, - ) - else - JSValue.jsNull(); + + const info = brk: { + if (arguments[0].as(Request)) |request| { + if (request.request_context.getRemoteSocketInfo()) |info| + break :brk info; + } else if (arguments[0].as(NodeHTTPResponse)) |response| { + if (!response.finished) { + if (response.response.getRemoteSocketInfo()) |info| { + break :brk info; + } + } + } + + return JSC.JSValue.jsNull(); + }; + + return JSSocketAddress__create( + globalObject, + bun.String.init(info.ip).toJS(this.globalThis), + info.port, + info.is_ipv6, + ); } - pub fn timeout(this: *ThisServer, request: *JSC.WebCore.Request, seconds: JSValue) JSC.JSValue { + pub fn timeout(this: *ThisServer, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + const arguments = callframe.arguments(2).slice(); + if (arguments.len < 2 or arguments[0].isEmptyOrUndefinedOrNull()) { + globalObject.throwNotEnoughArguments("timeout", 2, arguments.len); + return .zero; + } + + const seconds = arguments[1]; + + if (this.config.address == .unix) { + return JSValue.jsNull(); + } + if (!seconds.isNumber()) { this.globalThis.throw("timeout() requires a number", .{}); return .zero; } const value = seconds.to(c_uint); - _ = request.request_context.setTimeout(value); + + if (arguments[0].as(Request)) |request| { + _ = request.request_context.setTimeout(value); + } else if (arguments[0].as(NodeHTTPResponse)) |response| { + if (!response.finished) { + _ = response.response.timeout(@intCast(@min(value, 255))); + } + } else { + this.globalThis.throwInvalidArguments("timeout() requires a Request object", .{}); + return .zero; + } + return JSValue.jsUndefined(); } diff --git a/src/bun.js/bindings/NodeHTTP.cpp b/src/bun.js/bindings/NodeHTTP.cpp index 2554874a9cb98..87b59886d9ecf 100644 --- a/src/bun.js/bindings/NodeHTTP.cpp +++ b/src/bun.js/bindings/NodeHTTP.cpp @@ -21,9 +21,12 @@ namespace Bun { using namespace JSC; using namespace WebCore; +BUN_DECLARE_HOST_FUNCTION(jsFunctionRequestOrResponseHasBodyValue); +BUN_DECLARE_HOST_FUNCTION(jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer); extern "C" uWS::HttpRequest* Request__getUWSRequest(void*); extern "C" void Request__setInternalEventCallback(void*, EncodedJSValue, JSC::JSGlobalObject*); extern "C" void Request__setTimeout(void*, EncodedJSValue, JSC::JSGlobalObject*); +extern "C" void NodeHTTPResponse__setTimeout(void*, EncodedJSValue, JSC::JSGlobalObject*); extern "C" void Server__setIdleTimeout(EncodedJSValue, EncodedJSValue, JSC::JSGlobalObject*); static EncodedJSValue assignHeadersFromFetchHeaders(FetchHeaders& impl, JSObject* prototype, JSObject* objectValue, JSC::InternalFieldTuple* tuple, JSC::JSGlobalObject* globalObject, JSC::VM& vm) { @@ -758,6 +761,10 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetTimeout, (JSGlobalObject * globalObject, CallF Request__setTimeout(jsRequest->wrapped(), JSValue::encode(seconds), globalObject); } + if (auto* nodeHttpResponse = jsDynamicCast(requestValue)) { + NodeHTTPResponse__setTimeout(nodeHttpResponse->wrapped(), JSValue::encode(seconds), globalObject); + } + return JSValue::encode(jsUndefined()); } JSC_DEFINE_HOST_FUNCTION(jsHTTPSetServerIdleTimeout, (JSGlobalObject * globalObject, CallFrame* callFrame)) @@ -898,6 +905,13 @@ JSValue createNodeHTTPInternalBinding(Zig::GlobalObject* globalObject) obj->putDirect( vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "headersTuple"_s)), JSC::InternalFieldTuple::create(vm, globalObject->m_internalFieldTupleStructure.get()), 0); + obj->putDirectNativeFunction( + vm, globalObject, JSC::PropertyName(JSC::Identifier::fromString(vm, "webRequestOrResponseHasBodyValue"_s)), + 1, jsFunctionRequestOrResponseHasBodyValue, ImplementationVisibility::Public, Intrinsic::NoIntrinsic, 0); + + obj->putDirectNativeFunction( + vm, globalObject, JSC::PropertyName(JSC::Identifier::fromString(vm, "getCompleteWebRequestOrResponseBodyValueAsArrayBuffer"_s)), + 1, jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer, ImplementationVisibility::Public, Intrinsic::NoIntrinsic, 0); return obj; } diff --git a/src/bun.js/webcore/body.zig b/src/bun.js/webcore/body.zig index 3e95bf26974c7..9f2b4ee1504e2 100644 --- a/src/bun.js/webcore/body.zig +++ b/src/bun.js/webcore/body.zig @@ -299,6 +299,20 @@ pub const Body = struct { Error: ValueError, Null, + // We may not have all the data yet + // So we can't know for sure if it's empty or not + // We CAN know that it is definitely empty. + pub fn isDefinitelyEmpty(this: *const Value) bool { + return switch (this.*) { + .Null => true, + .Used, .Empty => true, + .InternalBlob => this.InternalBlob.slice().len == 0, + .Blob => this.Blob.size == 0, + .WTFStringImpl => this.WTFStringImpl.length() == 0, + .Error, .Locked => false, + }; + } + pub const heap_breakdown_label = "BodyValue"; pub const ValueError = union(enum) { AbortReason: JSC.CommonAbortReason, diff --git a/src/bun.js/webcore/request.zig b/src/bun.js/webcore/request.zig index 61c9c735e2f44..907f94bdb5721 100644 --- a/src/bun.js/webcore/request.zig +++ b/src/bun.js/webcore/request.zig @@ -119,10 +119,8 @@ pub const Request = struct { pub const InternalJSEventCallback = struct { function: JSC.Strong = .{}, - pub const EventType = enum(u8) { - timeout = 0, - abort = 1, - }; + pub const EventType = JSC.API.NodeHTTPResponse.AbortEvent; + pub fn init(function: JSC.JSValue, globalThis: *JSC.JSGlobalObject) InternalJSEventCallback { return InternalJSEventCallback{ .function = JSC.Strong.create(function, globalThis), diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 76d7d07aaa5e7..fe634fa345dd3 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -112,6 +112,58 @@ pub const Response = struct { return &this.body.value; } + pub export fn jsFunctionRequestOrResponseHasBodyValue(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + _ = globalObject; // autofix + const arguments = callframe.arguments(1); + const this_value = arguments.ptr[0]; + if (this_value.isEmptyOrUndefinedOrNull()) { + return .false; + } + + if (this_value.as(Response)) |response| { + return JSC.JSValue.jsBoolean(!response.body.value.isDefinitelyEmpty()); + } else if (this_value.as(Request)) |request| { + return JSC.JSValue.jsBoolean(!request.body.value.isDefinitelyEmpty()); + } + + return .false; + } + + pub export fn jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + const arguments = callframe.arguments(1); + const this_value = arguments.ptr[0]; + if (this_value.isEmptyOrUndefinedOrNull()) { + return .undefined; + } + + const body: *Body.Value = brk: { + if (this_value.as(Response)) |response| { + break :brk &response.body.value; + } else if (this_value.as(Request)) |request| { + break :brk &request.body.value; + } + + return .undefined; + }; + + // Get the body if it's available synchronously. + switch (body.*) { + .Used, .Empty, .Null => return .undefined, + .Blob => |*blob| { + if (blob.isBunFile()) { + return .undefined; + } + defer body.* = .{ .Used = {} }; + return blob.toArrayBuffer(globalObject, .transfer); + }, + .WTFStringImpl, .InternalBlob => { + var any_blob = body.useAsAnyBlob(); + return any_blob.toArrayBufferTransfer(globalObject); + }, + .Error, .Locked => return .undefined, + } + } + pub fn getFetchHeaders( this: *Response, ) ?*FetchHeaders { diff --git a/src/codegen/client-js.ts b/src/codegen/client-js.ts index 6ae7a3863a960..3f6918b74de76 100644 --- a/src/codegen/client-js.ts +++ b/src/codegen/client-js.ts @@ -9,7 +9,7 @@ let $debug_log_enabled = ((env) => ( // The rationale for checking all these variables is just so you don't have to exactly remember which one you set. (env.BUN_DEBUG_ALL && env.BUN_DEBUG_ALL !== '0') || (env.BUN_DEBUG_JS && env.BUN_DEBUG_JS !== '0') - || (env.BUN_DEBUG_${pathToUpperSnakeCase(filepath)}) + || (env.BUN_DEBUG_${pathToUpperSnakeCase(publicName)}) || (env.DEBUG_${pathToUpperSnakeCase(filepath)}) ))(Bun.env); let $debug_pid_prefix = Bun.env.SHOW_PID === '1'; diff --git a/src/deps/uws.zig b/src/deps/uws.zig index f15f8d651dc50..e1e2d34c9a7d7 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -3040,6 +3040,13 @@ pub const AnyResponse = union(enum) { SSL: *NewApp(true).Response, TCP: *NewApp(false).Response, + pub fn getRemoteSocketInfo(this: AnyResponse) ?SocketAddress { + return switch (this) { + .SSL => |resp| resp.getRemoteSocketInfo(), + .TCP => |resp| resp.getRemoteSocketInfo(), + }; + } + pub fn getWriteOffset(this: AnyResponse) u64 { return switch (this) { .SSL => |resp| resp.getWriteOffset(), @@ -3171,6 +3178,22 @@ pub const AnyResponse = union(enum) { }; } + pub fn onTimeout(this: AnyResponse, comptime UserDataType: type, comptime handler: fn (UserDataType, AnyResponse) void, opcional_data: UserDataType) void { + const wrapper = struct { + pub fn ssl_handler(user_data: UserDataType, resp: *NewApp(true).Response) void { + handler(user_data, .{ .SSL = resp }); + } + pub fn tcp_handler(user_data: UserDataType, resp: *NewApp(false).Response) void { + handler(user_data, .{ .TCP = resp }); + } + }; + + return switch (this) { + .SSL => |resp| resp.onTimeout(UserDataType, wrapper.ssl_handler, opcional_data), + .TCP => |resp| resp.onTimeout(UserDataType, wrapper.tcp_handler, opcional_data), + }; + } + pub fn onAborted(this: AnyResponse, comptime UserDataType: type, comptime handler: fn (UserDataType, AnyResponse) void, opcional_data: UserDataType) void { const wrapper = struct { pub fn ssl_handler(user_data: UserDataType, resp: *NewApp(true).Response) void { diff --git a/src/js/node/http.ts b/src/js/node/http.ts index a454bffc29575..f076ef6793f48 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -1,7 +1,19 @@ +const enum ClientRequestEmitState { + socket = 1, + prefinish = 2, + finish = 4, + response = 8, +} + +const enum NodeHTTPResponseAbortEvent { + none = 0, + abort = 1, + timeout = 2, +} const enum NodeHTTPIncomingRequestType { request, response, - onNodeHTTPRequest, + NodeHTTPResponse, } const enum NodeHTTPHeaderState { none, @@ -10,10 +22,12 @@ const enum NodeHTTPHeaderState { } const headerStateSymbol = Symbol("headerState"); +// used for pretending to emit events in the right order +const kEmitState = Symbol("emitState"); -const EventEmitter = require("node:events"); +const EventEmitter: typeof import("node:events").EventEmitter = require("node:events"); const { isTypedArray } = require("node:util/types"); -const { Duplex, Readable, Writable } = require("node:stream"); +const { Duplex, Readable, Stream } = require("node:stream"); const { ERR_INVALID_ARG_TYPE, ERR_INVALID_PROTOCOL } = require("internal/errors"); const { isPrimary } = require("internal/cluster/isPrimary"); const { kAutoDestroyed } = require("internal/shared"); @@ -32,6 +46,8 @@ const { Headers, Blob, headersTuple, + webRequestOrResponseHasBodyValue, + getCompleteWebRequestOrResponseBodyValueAsArrayBuffer, } = $cpp("NodeHTTP.cpp", "createNodeHTTPInternalBinding") as { getHeader: (headers: Headers, name: string) => string | undefined; setHeader: (headers: Headers, name: string, value: string) => void; @@ -44,6 +60,8 @@ const { Headers: (typeof globalThis)["Headers"]; Blob: (typeof globalThis)["Blob"]; headersTuple: any; + webRequestOrResponseHasBodyValue: (arg: any) => boolean; + getCompleteWebRequestOrResponseBodyValueAsArrayBuffer: (arg: any) => ArrayBuffer | undefined; }; let cluster; @@ -440,13 +458,14 @@ function Server(options, callback) { function onRequestEvent(event) { const [server, http_res, req] = this.socket[kInternalSocketData]; + // console.log({ event, finished: http_res[finishedSymbol] }); if (!http_res[finishedSymbol]) { switch (event) { - case 0: // timeout + case NodeHTTPResponseAbortEvent.timeout: this.emit("timeout"); server.emit("timeout", req.socket); break; - case 1: // abort + case NodeHTTPResponseAbortEvent.abort: this.complete = true; this.emit("close"); http_res[finishedSymbol] = true; @@ -656,6 +675,9 @@ Server.prototype = { handle, hasBody: boolean, ) { + const prevIsNextIncomingMessageHTTPS = isNextIncomingMessageHTTPS; + isNextIncomingMessageHTTPS = isHTTPS; + const http_req = new RequestClass( kInternalRequest, url, @@ -668,6 +690,7 @@ Server.prototype = { const http_res = new ResponseClass(http_req, { [kHandle]: handle, }); + isNextIncomingMessageHTTPS = prevIsNextIncomingMessageHTTPS; let capturedError; let rejectFunction; @@ -675,27 +698,48 @@ Server.prototype = { if (capturedError) return; capturedError = err; if (rejectFunction) rejectFunction(err); + handle && (handle.onabort = undefined); + handle = undefined; }; let resolveFunction; let didFinish = false; let closeCallback = () => { didFinish = true; + handle && (handle.onabort = undefined); if (resolveFunction) resolveFunction(http_res); + handle = undefined; }; http_req.once("error", errorCallback); http_res.once("error", errorCallback); + server.once("error", errorCallback); http_res.once("close", closeCallback); - server.emit("request", http_req, http_res); + handle.onabort = onRequestEvent.bind(http_req); + const socket = http_req.socket; + socket[kInternalSocketData] = [server, http_res, handle]; + + try { + server.emit("connection", socket); + server.emit("request", http_req, http_res); + } catch (err) { + errorCallback(err); + } if (capturedError) { + server.off("error", errorCallback); + http_res.off("error", errorCallback); + http_res.off("close", closeCallback); + handle = undefined; throw capturedError; } if (handle.finished || didFinish) { + server.off("error", errorCallback); + http_res.off("error", errorCallback); http_res.off("close", closeCallback); + handle = undefined; closeCallback = () => {}; return; } @@ -867,14 +911,38 @@ var noBodySymbol = Symbol("noBody"); var abortedSymbol = Symbol("aborted"); const isInternalRequestSymbol = Symbol("isInternalRequest"); +function emitEOFIncomingMessageOuter(self) { + self.push(null); + + process.nextTick(emitCloseNTAndComplete, self); +} +function emitEOFIncomingMessage(self) { + self[eofInProgress] = true; + process.nextTick(emitEOFIncomingMessageOuter, self); +} + +const eofInProgress = Symbol("eofInProgress"); + function IncomingMessage(req, defaultIncomingOpts) { this[abortedSymbol] = false; + this[eofInProgress] = false; this._consuming = false; this._dumped = false; this.complete = false; + this._closed = false; + + if (isNextIncomingMessageHTTPS) { + // Creating a new Duplex is expensive. + // We can skip it if the request is not HTTPS. + const socket = new FakeSocket(); + this[fakeSocketSymbol] = socket; + socket.encrypted = true; + isNextIncomingMessageHTTPS = false; + } // (url, method, headers, rawHeaders, handle, hasBody) if (req === kInternalRequest) { + this[typeSymbol] = NodeHTTPIncomingRequestType.NodeHTTPResponse; this.url = arguments[1]; this.method = arguments[2]; this.headers = arguments[3]; @@ -894,9 +962,6 @@ function IncomingMessage(req, defaultIncomingOpts) { this[bodyStreamSymbol] = undefined; - this.req = nodeReq; - this.req = nodeReq; - this.req = nodeReq; if (!assignHeaders(this, req)) { @@ -905,20 +970,13 @@ function IncomingMessage(req, defaultIncomingOpts) { this.url = reqUrl; } - if (isNextIncomingMessageHTTPS) { - // Creating a new Duplex is expensive. - // We can skip it if the request is not HTTPS. - const socket = new FakeSocket(); - this[fakeSocketSymbol] = socket; - socket.encrypted = true; - isNextIncomingMessageHTTPS = false; - } - this[noBodySymbol] = type === NodeHTTPIncomingRequestType.request // TODO: Add logic for checking for body on response ? requestHasNoBody(this.method, this) : false; } + + this._readableState.readingMore = true; } IncomingMessage.prototype = { @@ -927,53 +985,88 @@ IncomingMessage.prototype = { _construct(callback) { // TODO: streaming const type = this[typeSymbol]; - if (type === NodeHTTPIncomingRequestType.response || this[noBodySymbol]) { - callback(); - return; - } - if (type !== NodeHTTPIncomingRequestType.onNodeHTTPRequest) { - const contentLength = this.headers["content-length"]; - const length = contentLength ? parseInt(contentLength, 10) : 0; - if (length === 0) { - this[noBodySymbol] = true; - callback(); - return; + if (type === NodeHTTPIncomingRequestType.response) { + if (!webRequestOrResponseHasBodyValue(this[reqSymbol])) { + this.complete = true; + this.push(null); } + } - callback(); + callback(); + }, + // Call this instead of resume() if we want to just + // dump all the data to /dev/null + _dump() { + if (!this._dumped) { + this._dumped = true; + // If there is buffered data, it may trigger 'data' events. + // Remove 'data' event listeners explicitly. + this.removeAllListeners("data"); + this.resume(); } }, _read(size) { + if (!this._consuming) { + this._readableState.readingMore = false; + this._consuming = true; + } + + if (this[eofInProgress]) { + // There is a nextTick pending that will emit EOF + return; + } + let internalRequest; if (this[noBodySymbol]) { - this.complete = true; - this.push(null); + emitEOFIncomingMessage(this); + return; } else if ((internalRequest = this[kInternalRequest])) { - internalRequest.ondata = (chunk, isLast, isAborted) => { - $debug("ondata", chunk, isLast, isAborted); - if (isAborted) { - if (!this.destroyed) { - this.destroy(); - } + internalRequest.ondata = (chunk, isLast, aborted: NodeHTTPResponseAbortEvent) => { + $debug("ondata", chunk, isLast, aborted); + if (aborted === NodeHTTPResponseAbortEvent.abort) { + this.destroy(); return; } - this.push(chunk); + + if (chunk && !this._dumped) this.push(chunk); + if (isLast) { - this.complete = true; - this.push(null); + emitEOFIncomingMessage(this); } }; + + if (!internalRequest.hasBody) { + emitEOFIncomingMessage(this); + } + + return true; } else if (this[bodyStreamSymbol] == null) { - const reader = this[reqSymbol].body?.getReader() as ReadableStreamDefaultReader; + // If it's all available right now, we skip going through ReadableStream. + let completeBody = getCompleteWebRequestOrResponseBodyValueAsArrayBuffer(this[reqSymbol]); + if (completeBody) { + $assert(completeBody instanceof ArrayBuffer, "completeBody is not an ArrayBuffer"); + $assert(completeBody.byteLength > 0, "completeBody should not be empty"); + + // They're ignoring the data. Let's not do anything with it. + if (!this._dumped) { + this.push(new Buffer(completeBody)); + } + emitEOFIncomingMessage(this); + return; + } + + const reader = this[reqSymbol].body?.getReader?.() as ReadableStreamDefaultReader; if (!reader) { - this.complete = true; - this.push(null); + emitEOFIncomingMessage(this); return; } + this[bodyStreamSymbol] = reader; consumeStream(this, reader); } + + return; }, _finish() { this.emit("prefinish"); @@ -1010,7 +1103,7 @@ IncomingMessage.prototype = { socket.destroy(err); } - if (cb) { + if (cb && err) { emitErrorNextTick(this, err, cb); } }, @@ -1067,7 +1160,9 @@ IncomingMessage.prototype = { // noop }, setTimeout(msecs, callback) { - const req = this[reqSymbol]; + this.take; + const req = this[reqSymbol] || this[kInternalRequest]; + if (req) { setRequestTimeout(req, Math.ceil(msecs / 1000)); typeof callback === "function" && this.once("timeout", callback); @@ -1080,7 +1175,7 @@ IncomingMessage.prototype = { set socket(value) { this[fakeSocketSymbol] = value; }, -}; +} satisfies typeof import("node:http").IncomingMessage.prototype; $setPrototypeDirect.$call(IncomingMessage, Readable); async function consumeStream(self, reader: ReadableStreamDefaultReader) { @@ -1099,8 +1194,10 @@ async function consumeStream(self, reader: ReadableStreamDefaultReader) { if (self.destroyed || (aborted = self[abortedSymbol])) { break; } - for (var v of value) { - self.push(v); + if (!self._dumped) { + for (var v of value) { + self.push(v); + } } if (self.destroyed || (aborted = self[abortedSymbol]) || done) { @@ -1115,8 +1212,7 @@ async function consumeStream(self, reader: ReadableStreamDefaultReader) { } if (!self.complete) { - self.complete = true; - self.push(null); + emitEOFIncomingMessage(self); } } @@ -1125,17 +1221,16 @@ const finishedSymbol = Symbol("finished"); const timeoutTimerSymbol = Symbol("timeoutTimer"); const fakeSocketSymbol = Symbol("fakeSocket"); function OutgoingMessage(options) { - Writable.$call(this, options); + Stream.$call(this, options); this.headersSent = false; this.sendDate = true; this[finishedSymbol] = false; this[headerStateSymbol] = NodeHTTPHeaderState.none; this[kAbortController] = null; } - const OutgoingMessagePrototype = { constructor: OutgoingMessage, - __proto__: Writable.prototype, + __proto__: Stream.prototype, appendHeader(name, value) { var headers = (this[headersSymbol] ??= new Headers()); @@ -1195,8 +1290,8 @@ const OutgoingMessagePrototype = { clearTimeout(this[timeoutTimerSymbol]); if (msecs === 0) { - if (callback !== undefined) { - validateFunction(callback, "callback"); + if (callback != null) { + if (!$isCallable(callback)) validateFunction(callback, "callback"); this.removeListener("timeout", callback); } @@ -1204,8 +1299,8 @@ const OutgoingMessagePrototype = { } else { this[timeoutTimerSymbol] = setTimeout(onTimeout.bind(this), msecs).unref(); - if (callback !== undefined) { - validateFunction(callback, "callback"); + if (callback != null) { + if (!$isCallable(callback)) validateFunction(callback, "callback"); this.once("timeout", callback); } } @@ -1213,10 +1308,6 @@ const OutgoingMessagePrototype = { return this; }, - get writableEnded() { - return this[finishedSymbol]; - }, - get connection() { return this.socket; }, @@ -1246,17 +1337,59 @@ const OutgoingMessagePrototype = { // noop }, + get writableObjectMode() { + return false; + }, + + get writableLength() { + return 0; + }, + + get writableHighWaterMark() { + return 16 * 1024; + }, + + get writableNeedDrain() { + return !this.destroyed && !this[finishedSymbol]; + }, + + get writableEnded() { + return this[finishedSymbol]; + }, + + get writableFinished() { + return this[finishedSymbol]; + }, + _send(data, encoding, callback, byteLength) { + if (this.destroyed) { + return false; + } return this.write(data, encoding, callback); }, + end(chunk, encoding, callback) {}, } satisfies typeof import("node:http").OutgoingMessage.prototype; OutgoingMessage.prototype = OutgoingMessagePrototype; -$setPrototypeDirect.$call(OutgoingMessage, Writable); +$setPrototypeDirect.$call(OutgoingMessage, Stream); + +function onTimeout() { + this[timeoutTimerSymbol] = undefined; + this[kAbortController]?.abort(); + const handle = this[kHandle]; + + this.emit("timeout"); + if (handle) { + handle.abort(); + } +} function emitContinueAndSocketNT(self) { if (self.destroyed) return; // Ref: https://github.com/nodejs/node/blob/f63e8b7fa7a4b5e041ddec67307609ec8837154f/lib/_http_client.js#L803-L839 - self.emit("socket", self.socket); + if (!(self[kEmitState] & ClientRequestEmitState.socket)) { + self[kEmitState] |= ClientRequestEmitState.socket; + self.emit("socket", self.socket); + } //Emit continue event for the client (internally we auto handle it) if (!self._closed && self.getHeader("expect") === "100-continue") { @@ -1270,6 +1403,15 @@ function emitCloseNT(self) { } } +function emitCloseNTAndComplete(self) { + if (!self._closed) { + self._closed = true; + self.emit("close"); + } + + self.complete = true; +} + function emitRequestCloseNT(self) { self.emit("close"); } @@ -1304,15 +1446,15 @@ const firstWriteSymbol = Symbol("firstWrite"); const deferredSymbol = Symbol("deferred"); const kDeprecatedReplySymbol = Symbol("deprecatedReply"); const kHandle = Symbol("handle"); +const closedSymbol = Symbol("closed"); function ServerResponse(req, options) { if ((this[kDeprecatedReplySymbol] = options?.[kDeprecatedReplySymbol])) { this[controllerSymbol] = undefined; this[firstWriteSymbol] = undefined; this[deferredSymbol] = undefined; - this._writev = ServerResponse_writevDeprecated; - this._write = ServerResponse_writeDeprecated; - this._final = ServerResponse_finalDeprecated; + this.write = ServerResponse_writeDeprecated; + this.end = ServerResponse_finalDeprecated; } OutgoingMessage.$call(this, options); @@ -1321,7 +1463,6 @@ function ServerResponse(req, options) { this.sendDate = true; this.statusCode = 200; this.statusMessage = undefined; - this._writableState.decodeStrings = false; this._sent100 = false; this._defaultKeepAlive = false; this._removedConnection = false; @@ -1338,18 +1479,27 @@ function ServerResponse(req, options) { if (handle) { this[kHandle] = handle; - handle.onabort = () => this.destroy(); } } + +function callWriteHeadIfObservable(self, headerState) { + if ( + headerState === NodeHTTPHeaderState.none && + !(self.writeHead === OriginalWriteHeadFn && self._implicitHeader === OriginalImplicitHeadFn) + ) { + self.writeHead(self.statusCode, self.statusMessage, self[headersSymbol]); + } +} + const ServerResponsePrototype = { constructor: ServerResponse, __proto__: OutgoingMessage.prototype, get headersSent() { - return this[headerStateSymbol] === NodeHTTPHeaderState.assigned; + return this[headerStateSymbol] === NodeHTTPHeaderState.sent; }, set headersSent(value) { - this[headerStateSymbol] = value ? NodeHTTPHeaderState.assigned : NodeHTTPHeaderState.none; + this[headerStateSymbol] = value ? NodeHTTPHeaderState.sent : NodeHTTPHeaderState.none; }, // This end method is actually on the OutgoingMessage prototype in Node.js @@ -1360,25 +1510,29 @@ const ServerResponsePrototype = { return this; } - const isFinished = this.finished; + const handle = this[kHandle]; + const isFinished = this.finished || handle?.finished; + + if ($isCallable(chunk)) { + callback = chunk; + chunk = undefined; + encoding = undefined; + } else if ($isCallable(encoding)) { + callback = encoding; + encoding = undefined; + } else if (!$isCallable(callback)) { + callback = undefined; + } if (isFinished && chunk) { emitErrorNextTick(this, $ERR_STREAM_WRITE_AFTER_END("Stream is already finished"), callback); return this; } - if (isFinished && $isCallable(callback)) { - emitErrorNextTick(this, $ERR_STREAM_ALREADY_FINISHED("Stream is already finished"), callback); - return this; - } - - if (!$isCallable(callback) && callback !== undefined) { - callback = undefined; - } - - const handle = this[kHandle]; if (handle) { const headerState = this[headerStateSymbol]; + callWriteHeadIfObservable(this, headerState); + if (headerState !== NodeHTTPHeaderState.sent) { handle.cork(() => { handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]); @@ -1391,9 +1545,48 @@ const ServerResponsePrototype = { this._contentLength = handle.end(chunk, encoding); }); } else { - handle.end(chunk, encoding); + // If there's no data but you already called end, then you're done. + // We can ignore it in that case. + if (!(!chunk && handle.ended)) { + handle.end(chunk, encoding); + } + } + this[finishedSymbol] = this.finished = true; + + this.emit("prefinish"); + + if (callback) { + process.nextTick( + function (callback, self) { + // In Node.js, the "finish" event triggers the "close" event. + // So it shouldn't become closed === true until after "finish" is emitted and the callback is called. + self.emit("finish"); + + try { + callback(); + } catch (err) { + self.emit("error", err); + } + + process.nextTick(function (self) { + if (!self[closedSymbol]) { + self[closedSymbol] = true; + self.emit("close"); + } + }, self); + }, + callback, + this, + ); + } else { + this.emit("finish"); + process.nextTick(function (self) { + if (!self[closedSymbol]) { + self[closedSymbol] = true; + self.emit("close"); + } + }, this); } - this.finished = true; } return this; @@ -1401,11 +1594,19 @@ const ServerResponsePrototype = { write(chunk, encoding, callback) { const handle = this[kHandle]; - if (!handle) { - return OutgoingMessagePrototype.write.$apply(this, arguments); + + if ($isCallable(chunk)) { + callback = chunk; + chunk = undefined; + encoding = undefined; + } else if ($isCallable(encoding)) { + callback = encoding; + encoding = undefined; + } else if (!$isCallable(callback)) { + callback = undefined; } - if (this.destroyed) { + if (this.destroyed || !handle) { if ($isCallable(callback)) { callback($ERR_STREAM_DESTROYED("Stream is destroyed")); } @@ -1419,9 +1620,8 @@ const ServerResponsePrototype = { let result = 0; - if (!$isCallable(callback)) { - callback = undefined; - } + const headerState = this[headerStateSymbol]; + callWriteHeadIfObservable(this, headerState); if (this[headerStateSymbol] !== NodeHTTPHeaderState.sent) { handle.cork(() => { @@ -1438,15 +1638,18 @@ const ServerResponsePrototype = { } if (result < 0) { - handle.onwritable = () => { - if (!this.finished && !this.destroyed) { - this.emit("drain"); - } - }; + handle.onwritable = callback + ? ServerResponsePrototypeOnWritable.bind(this, callback) + : ServerResponsePrototypeOnWritable.bind(this); return false; } - if (result > 0) this.emit("drain"); + if (result > 0) { + if (callback) { + process.nextTick(callback); + } + this.emit("drain"); + } return true; }, @@ -1501,7 +1704,24 @@ const ServerResponsePrototype = { }, get writableNeedDrain() { - return !this.destroyed && !this[finishedSymbol] && (this[kHandle]?.bufferedAmount ?? 0) === 0; + return !this.destroyed && !this[finishedSymbol] && (this[kHandle]?.bufferedAmount ?? 1) !== 0; + }, + + get writableFinished() { + const isWritableFinished = this[finishedSymbol] && (!this[kHandle] || this[kHandle].finished); + return isWritableFinished; + }, + + get writableLength() { + return 16 * 1024; + }, + + get writableHighWaterMark() { + return 64 * 1024; + }, + + get closed() { + return this[closedSymbol]; }, _send(data, encoding, callback, byteLength) { @@ -1537,7 +1757,6 @@ const ServerResponsePrototype = { socket._httpMessage = this; socket.on("close", () => onServerResponseClose.$call(socket)); this.socket = socket; - this._writableState.autoDestroy = false; this.emit("socket", socket); }, @@ -1575,6 +1794,7 @@ const ServerResponsePrototype = { }, } satisfies typeof import("node:http").ServerResponse.prototype; ServerResponse.prototype = ServerResponsePrototype; +$setPrototypeDirect.$call(ServerResponse, Stream); const ServerResponse_writeDeprecated = function _write(chunk, encoding, callback) { if (this[firstWriteSymbol] === undefined && !this.headersSent) { @@ -1628,12 +1848,46 @@ function drainHeadersIfObservable() { this._implicitHeader(); } -function ServerResponse_finalDeprecated(callback) { +function ServerResponsePrototypeOnWritable(this: import("node:http").ServerResponse, optionalCallback) { + if (optionalCallback) { + optionalCallback(); + } + + if (!this.finished && !this.destroyed) { + this.emit("drain"); + } +} + +function ServerResponse_finalDeprecated(chunk, encoding, callback) { + if ($isCallable(encoding)) { + callback = encoding; + encoding = undefined; + } + if (!$isCallable(callback)) { + callback = undefined; + } + if (this.destroyed || this.finished) { + if (callback) callback(); + return; + } + const req = this.req; const shouldEmitClose = req && req.emit && !this[finishedSymbol]; if (!this.headersSent) { - var data = this[firstWriteSymbol] || ""; + var data = this[firstWriteSymbol]; + if (chunk) { + if (data) { + if (encoding) { + data = Buffer.from(data, encoding); + } + + data = new Blob([data, chunk]); + } + } else if (!data) { + data = undefined; + } + this[firstWriteSymbol] = undefined; this[finishedSymbol] = true; this.headersSent = true; // https://github.com/oven-sh/bun/issues/3458 @@ -1655,17 +1909,27 @@ function ServerResponse_finalDeprecated(callback) { this[finishedSymbol] = true; ensureReadableStreamController.$call(this, controller => { - controller.end(); - if (shouldEmitClose) { - req.complete = true; - process.nextTick(emitRequestCloseNT, req); + if (chunk && encoding) { + chunk = Buffer.from(chunk, encoding); } - callback(); - const deferred = this[deferredSymbol]; - if (deferred) { - this[deferredSymbol] = undefined; - deferred(); + + let prom; + if (chunk) { + prom = controller.end(chunk); + } else { + prom = controller.end(); } + + const handler = () => { + callback(); + const deferred = this[deferredSymbol]; + if (deferred) { + this[deferredSymbol] = undefined; + deferred(); + } + }; + if ($isPromise(prom)) prom.then(handler, handler); + else handler(); }); } @@ -1676,7 +1940,7 @@ ServerResponse.prototype.writeHeader = ServerResponse.prototype.writeHead; OriginalWriteHeadFn = ServerResponse.prototype.writeHead; OriginalImplicitHeadFn = ServerResponse.prototype._implicitHeader; -class ClientRequest extends OutgoingMessage { +class ClientRequest extends (OutgoingMessage as unknown as typeof import("node:http").OutgoingMessage) { #timeout; #res: IncomingMessage | null = null; #upgradeOrConnect = false; @@ -1701,7 +1965,7 @@ class ClientRequest extends OutgoingMessage { [kAbortController]: AbortController | null = null; #timeoutTimer?: Timer = undefined; #options; - #finished; + [finishedSymbol] = false; _httpMessage; @@ -1729,32 +1993,80 @@ class ClientRequest extends OutgoingMessage { return this.#agent; } - _write(chunk, encoding, callback) { + write(chunk, encoding, callback) { + if (this.destroyed) return false; + if ($isCallable(chunk)) { + callback = chunk; + chunk = undefined; + encoding = undefined; + } else if ($isCallable(encoding)) { + callback = encoding; + encoding = undefined; + } else if (!$isCallable(callback)) { + callback = undefined; + } + + return this.#write(chunk, encoding, callback); + } + + #write(chunk, encoding, callback) { + const canSkipReEncodingData = + // UTF-8 string: + (typeof chunk === "string" && (encoding === "utf-8" || encoding === "utf8" || !encoding)) || + // Buffer + ($isTypedArrayView(chunk) && (!encoding || encoding === "buffer" || encoding === "utf-8")); + + if (!canSkipReEncodingData) { + chunk = Buffer.from(chunk, encoding); + } + if (!this.#bodyChunks) { this.#bodyChunks = [chunk]; - callback(); - return; + if (callback) callback(); + return true; } this.#bodyChunks.push(chunk); - callback(); + if (callback) callback(); + return true; } - _writev(chunks, callback) { - if (!this.#bodyChunks) { - this.#bodyChunks = chunks; - callback(); - return; + end(chunk, encoding, callback) { + if ($isCallable(chunk)) { + callback = chunk; + chunk = undefined; + encoding = undefined; + } else if ($isCallable(encoding)) { + callback = encoding; + encoding = undefined; + } else if (!$isCallable(callback)) { + callback = undefined; } - this.#bodyChunks.push(...chunks); - callback(); + + if (chunk) { + if (this[finishedSymbol]) { + emitErrorNextTick(this, $ERR_STREAM_WRITE_AFTER_END("Cannot write after end"), callback); + return this; + } + + this.#write(chunk, encoding, callback); + } + + if (callback) { + this.once("finish", callback); + } + + this.#send(); } - _destroy(err, callback) { + destroy(err?: Error) { + if (this.destroyed) return this; this.destroyed = true; + this[finishedSymbol] = true; // If request is destroyed we abort the current response this[kAbortController]?.abort?.(); - this.socket.destroy(); - emitErrorNextTick(this, err, callback); + this.socket.destroy(err); + + return this; } _ensureTls() { @@ -1762,8 +2074,8 @@ class ClientRequest extends OutgoingMessage { return this.#tls; } - _final(callback) { - this.#finished = true; + #send() { + this[finishedSymbol] = true; this[kAbortController] = new AbortController(); this[kAbortController].signal.addEventListener( "abort", @@ -1780,7 +2092,10 @@ class ClientRequest extends OutgoingMessage { } var method = this.#method, - body = this.#bodyChunks?.length === 1 ? this.#bodyChunks[0] : Buffer.concat(this.#bodyChunks || []); + body = this.#bodyChunks && this.#bodyChunks.length > 1 ? new Blob(this.#bodyChunks) : this.#bodyChunks?.[0]; + if (body) { + this.#bodyChunks = []; + } let url: string; let proxy: string | undefined; @@ -1841,7 +2156,6 @@ class ClientRequest extends OutgoingMessage { fetchOptions.unix = socketPath; } - this._writableState.autoDestroy = false; //@ts-ignore this.#fetchRequest = fetch(url, fetchOptions) .then(response => { @@ -1852,11 +2166,29 @@ class ClientRequest extends OutgoingMessage { const prevIsHTTPS = isNextIncomingMessageHTTPS; isNextIncomingMessageHTTPS = response.url.startsWith("https:"); var res = (this.#res = new IncomingMessage(response, { - type: NodeHTTPIncomingRequestType.response, + [typeSymbol]: NodeHTTPIncomingRequestType.response, [kInternalRequest]: this, })); isNextIncomingMessageHTTPS = prevIsHTTPS; - this.emit("response", res); + res.req = this; + + process.nextTick( + (self, res) => { + // If the user did not listen for the 'response' event, then they + // can't possibly read the data, so we ._dump() it into the void + // so that the socket doesn't hang there in a paused state. + if (!self.emit("response", res)) { + res._dump(); + } + }, + this, + res, + ); + + if (res.statusCode === 304) { + res.complete = true; + return; + } }) .catch(err => { // Node treats AbortError separately. @@ -1872,13 +2204,46 @@ class ClientRequest extends OutgoingMessage { .finally(() => { this.#fetchRequest = null; this[kClearTimeout](); - emitCloseNT(this); }); } catch (err) { if (!!$debug) globalReportError(err); this.emit("error", err); } finally { - callback(); + this.#maybeEmitClose(); + } + } + + // --- For faking the events in the right order --- + #maybeEmitSocket() { + if (!(this[kEmitState] & ClientRequestEmitState.socket)) { + this[kEmitState] |= ClientRequestEmitState.socket; + this.emit("socket", this.socket); + } + } + + #maybeEmitPrefinish() { + this.#maybeEmitSocket(); + + if (!(this[kEmitState] & ClientRequestEmitState.prefinish)) { + this[kEmitState] |= ClientRequestEmitState.prefinish; + this.emit("prefinish"); + } + } + + #maybeEmitFinish() { + this.#maybeEmitPrefinish(); + + if (!(this[kEmitState] & ClientRequestEmitState.finish)) { + this[kEmitState] |= ClientRequestEmitState.finish; + this.emit("finish"); + } + } + + #maybeEmitClose() { + this.#maybeEmitPrefinish(); + + if (!this._closed) { + process.nextTick(emitCloseNTAndComplete, this); } } @@ -1934,6 +2299,7 @@ class ClientRequest extends OutgoingMessage { throw ERR_INVALID_ARG_TYPE("options.agent", "Agent-like Object, undefined, or false", agent); } this.#agent = agent; + this.destroyed = false; const protocol = options.protocol || defaultAgent.protocol; let expectedProtocol = defaultAgent.protocol; @@ -2085,7 +2451,7 @@ class ClientRequest extends OutgoingMessage { // this.useChunkedEncodingByDefault = true; // } - this.#finished = false; + this[finishedSymbol] = false; this.#res = null; this.#upgradeOrConnect = false; this.#parser = null; @@ -2161,6 +2527,8 @@ class ClientRequest extends OutgoingMessage { process.nextTick(emitContinueAndSocketNT, this); } + [kEmitState]: number = 0; + setSocketKeepAlive(enable = true, initialDelay = 0) { $debug(`${NODE_HTTP_WARNING}\n`, "WARN: ClientRequest.setSocketKeepAlive is a no-op"); } @@ -2497,10 +2865,12 @@ function get(url, options, cb) { } function onError(self, error, cb) { - if (error) { + if ($isCallable(cb)) { cb(error); - } else { - cb(); + } + + if (typeof self.emit === "function" && !self._closed) { + self.emit("error", error); } } diff --git a/test/js/node/http/fixtures/log-events.mjs b/test/js/node/http/fixtures/log-events.mjs index bb5dcdd820afc..ef087634dcb38 100644 --- a/test/js/node/http/fixtures/log-events.mjs +++ b/test/js/node/http/fixtures/log-events.mjs @@ -10,7 +10,7 @@ const options = { const req = http.request(options, res => { patchEmitter(res, "res"); - console.log(`STATUS: ${res.statusCode}`); + console.log(`"STATUS: ${res.statusCode}"`); res.setEncoding("utf8"); }); patchEmitter(req, "req"); @@ -21,7 +21,10 @@ function patchEmitter(emitter, prefix) { var oldEmit = emitter.emit; emitter.emit = function () { - console.log([prefix, arguments[0]]); + if (typeof arguments[0] !== "symbol") { + console.log([prefix, arguments[0]]); + } + oldEmit.apply(emitter, arguments); }; } diff --git a/test/js/node/http/node-http.test.ts b/test/js/node/http/node-http.test.ts index a3f4e2f76775d..35569607481ac 100644 --- a/test/js/node/http/node-http.test.ts +++ b/test/js/node/http/node-http.test.ts @@ -77,10 +77,8 @@ describe("node:http", () => { } }); it("request & response body streaming (large)", async () => { + const input = Buffer.alloc("hello world, hello world".length * 9000, "hello world, hello world"); try { - const bodyBlob = new Blob(["hello world", "hello world".repeat(9000)]); - const input = await bodyBlob.text(); - var server = createServer((req, res) => { res.writeHead(200, { "Content-Type": "text/plain" }); req.on("data", chunk => { @@ -94,11 +92,11 @@ describe("node:http", () => { const url = await listen(server); const res = await fetch(url, { method: "POST", - body: bodyBlob, + body: input, }); const out = await res.text(); - expect(out).toBe(input); + expect(out).toBe(input.toString()); } finally { server.close(); } @@ -416,6 +414,7 @@ describe("node:http", () => { const req = request(`http://localhost:${port}`, res => { let data = ""; res.setEncoding("utf8"); + res.on("data", chunk => { data += chunk; }); @@ -1279,22 +1278,7 @@ describe("server.address should be valid IP", () => { done(err); } }); - test("ServerResponse reply", done => { - const createDone = createDoneDotAll(done); - const doneRequest = createDone(); - try { - const req = {}; - const sendedText = "Bun\n"; - const res = new ServerResponse(req, async (res: Response) => { - expect(await res.text()).toBe(sendedText); - doneRequest(); - }); - res.write(sendedText); - res.end(); - } catch (err) { - doneRequest(err); - } - }); + test("ServerResponse instanceof OutgoingMessage", () => { expect(new ServerResponse({}) instanceof OutgoingMessage).toBe(true); }); @@ -1885,19 +1869,17 @@ it("should emit events in the right order", async () => { expect(err).toBeEmpty(); const out = await new Response(stdout).text(); // TODO prefinish and socket are not emitted in the right order - expect(out.split("\n")).toEqual([ - `[ "req", "prefinish" ]`, - `[ "req", "socket" ]`, - `[ "req", "finish" ]`, - `[ "req", "response" ]`, + expect(out.split("\n").filter(Boolean).map(JSON.parse)).toStrictEqual([ + ["req", "socket"], + ["req", "prefinish"], + ["req", "finish"], + ["req", "response"], "STATUS: 200", - // `[ "res", "resume" ]`, - // `[ "res", "readable" ]`, - // `[ "res", "end" ]`, - `[ "req", "close" ]`, - `[ "res", Symbol(kConstruct) ]`, - // `[ "res", "close" ]`, - "", + ["res", "resume"], + ["res", "readable"], + ["res", "end"], + ["req", "close"], + ["res", "close"], ]); }); @@ -2332,6 +2314,7 @@ it("should emit close, and complete should be true only after close #13373", asy const [req, res] = await once(server, "request"); expect(req.complete).toBe(false); + console.log("ok 1"); const closeEvent = once(req, "close"); res.end("hi"); diff --git a/test/js/web/fetch/fetch.stream.test.ts b/test/js/web/fetch/fetch.stream.test.ts index 21a72ede53362..301e6a9b0c791 100644 --- a/test/js/web/fetch/fetch.stream.test.ts +++ b/test/js/web/fetch/fetch.stream.test.ts @@ -221,7 +221,7 @@ describe("fetch() with streaming", () => { expect(true).toBe(true); } finally { - server?.close(); + server?.closeAllConnections(); } }); } diff --git a/test/regression/issue/04298/node-fixture.mjs b/test/regression/issue/04298/node-fixture.mjs new file mode 100644 index 0000000000000..cfbdca3c11c95 --- /dev/null +++ b/test/regression/issue/04298/node-fixture.mjs @@ -0,0 +1,41 @@ +const { spawn } = await import("child_process"); +const assert = await import("assert"); +const http = await import("http"); + +async function runTest() { + return new Promise((resolve, reject) => { + const server = spawn("node", ["04298.fixture.js"], { + cwd: import.meta.dirname, + stdio: ["inherit", "inherit", "inherit", "ipc"], + }); + + server.on("message", url => { + http + .get(url, res => { + assert.strictEqual(res.statusCode, 500); + server.kill(); + resolve(); + }) + .on("error", reject); + }); + + server.on("error", reject); + server.on("exit", (code, signal) => { + if (code !== null && code !== 0) { + reject(new Error(`Server exited with code ${code}`)); + } else if (signal) { + reject(new Error(`Server was killed with signal ${signal}`)); + } + }); + }); +} + +runTest() + .then(() => { + console.log("Test passed"); + process.exit(0); + }) + .catch(error => { + console.error("Test failed:", error); + process.exit(1); + }); From 44f1870b17c926a663166af297f67c76d4e9aaed Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 7 Oct 2024 07:13:37 -0700 Subject: [PATCH 06/36] tweaks --- src/js/node/http.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/js/node/http.ts b/src/js/node/http.ts index f076ef6793f48..90d18d9f2e590 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -913,7 +913,7 @@ const isInternalRequestSymbol = Symbol("isInternalRequest"); function emitEOFIncomingMessageOuter(self) { self.push(null); - + self.complete = true; process.nextTick(emitCloseNTAndComplete, self); } function emitEOFIncomingMessage(self) { @@ -2160,6 +2160,7 @@ class ClientRequest extends (OutgoingMessage as unknown as typeof import("node:h this.#fetchRequest = fetch(url, fetchOptions) .then(response => { if (this.aborted) { + this.#maybeEmitClose(); return; } @@ -2184,7 +2185,7 @@ class ClientRequest extends (OutgoingMessage as unknown as typeof import("node:h this, res, ); - + this.#maybeEmitClose(); if (res.statusCode === 304) { res.complete = true; return; @@ -2209,7 +2210,7 @@ class ClientRequest extends (OutgoingMessage as unknown as typeof import("node:h if (!!$debug) globalReportError(err); this.emit("error", err); } finally { - this.#maybeEmitClose(); + this.#maybeEmitFinish(); } } @@ -2547,7 +2548,7 @@ class ClientRequest extends (OutgoingMessage as unknown as typeof import("node:h #onTimeout() { this.#timeoutTimer = undefined; - this[kAbortController]?.abort(); + this[kAbortController]?.abort?.(); this.emit("timeout"); } From 4a53dee5c130ec1211563541e6b3b2c8325c52f9 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 7 Oct 2024 07:23:22 -0700 Subject: [PATCH 07/36] Update http.ts --- src/js/node/http.ts | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/js/node/http.ts b/src/js/node/http.ts index 90d18d9f2e590..324ea8f0b82a8 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -458,7 +458,7 @@ function Server(options, callback) { function onRequestEvent(event) { const [server, http_res, req] = this.socket[kInternalSocketData]; - // console.log({ event, finished: http_res[finishedSymbol] }); + if (!http_res[finishedSymbol]) { switch (event) { case NodeHTTPResponseAbortEvent.timeout: @@ -713,30 +713,19 @@ Server.prototype = { http_req.once("error", errorCallback); http_res.once("error", errorCallback); - server.once("error", errorCallback); - http_res.once("close", closeCallback); handle.onabort = onRequestEvent.bind(http_req); const socket = http_req.socket; socket[kInternalSocketData] = [server, http_res, handle]; - - try { - server.emit("connection", socket); - server.emit("request", http_req, http_res); - } catch (err) { - errorCallback(err); - } + server.emit("connection", socket); + server.emit("request", http_req, http_res); if (capturedError) { - server.off("error", errorCallback); - http_res.off("error", errorCallback); - http_res.off("close", closeCallback); handle = undefined; throw capturedError; } if (handle.finished || didFinish) { - server.off("error", errorCallback); http_res.off("error", errorCallback); http_res.off("close", closeCallback); handle = undefined; @@ -2058,9 +2047,10 @@ class ClientRequest extends (OutgoingMessage as unknown as typeof import("node:h this.#send(); } - destroy(err?: Error) { + _destroy(err?: Error, callback) { if (this.destroyed) return this; this.destroyed = true; + this[finishedSymbol] = true; // If request is destroyed we abort the current response this[kAbortController]?.abort?.(); @@ -2188,6 +2178,7 @@ class ClientRequest extends (OutgoingMessage as unknown as typeof import("node:h this.#maybeEmitClose(); if (res.statusCode === 304) { res.complete = true; + this.#maybeEmitClose(); return; } }) From 1f93522b88cc4b83240af2005283e93fbf4e6af0 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 7 Oct 2024 18:46:27 -0700 Subject: [PATCH 08/36] Automatically stop reading body --- src/bun.js/api/server.zig | 31 +++++++++++++++++++++-------- test/js/node/http/node-http.test.ts | 3 ++- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 7dbb7f172b871..5ed8dd2427c37 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -5823,6 +5823,17 @@ pub const NodeHTTPResponse = struct { done, }; + pub fn maybeStopReadingBody(this: *NodeHTTPResponse, vm: *JSC.VirtualMachine) void { + if (this.finished and this.ended and this.body_read_ref.has and this.body_read_state == .pending and !this.onDataCallback.has()) { + this.response.clearOnData(); + this.body_read_ref.unref(vm); + this.body_read_state = .done; + const server = this.server; + this.deref(); + server.onRequestComplete(); + } + } + pub fn create( any_server_tag: u64, globalObject: *JSC.JSGlobalObject, @@ -6099,19 +6110,22 @@ pub const NodeHTTPResponse = struct { } pub export fn Bun__NodeHTTPRequest__onResolve(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { - _ = globalObject; // autofix log("onResolve", .{}); const arguments = callframe.arguments(2).slice(); const this: *NodeHTTPResponse = arguments[1].as(NodeHTTPResponse).?; this.promise.deinit(); defer this.deref(); + this.maybeStopReadingBody(globalObject.bunVM()); + if (!this.finished and !this.aborted) { this.clearJSValues(); this.response.clearAborted(); this.response.clearOnData(); this.response.clearOnWritable(); this.response.clearTimeout(); - this.response.endWithoutBody(this.response.state().isHttpConnectionClose()); + if (this.response.state().isResponsePending()) { + this.response.endWithoutBody(this.response.state().isHttpConnectionClose()); + } this.onRequestComplete(); } @@ -6123,6 +6137,8 @@ pub const NodeHTTPResponse = struct { const err = arguments[0]; const this: *NodeHTTPResponse = arguments[1].as(NodeHTTPResponse).?; this.promise.deinit(); + this.maybeStopReadingBody(globalObject.bunVM()); + defer this.deref(); if (!this.finished and !this.aborted) { @@ -7711,12 +7727,6 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp if (node_http_response) |node_response| { if (!node_response.finished and node_response.response.state().isResponsePending()) { - node_response.clearJSValues(); - node_response.response.clearAborted(); - node_response.response.clearOnData(); - node_response.response.clearTimeout(); - node_response.response.clearOnWritable(); - node_response.finished = true; if (node_response.response.state().isHttpStatusCalled()) { node_response.response.writeStatus("500 Internal Server Error"); node_response.response.endWithoutBody(true); @@ -7724,6 +7734,7 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp node_response.response.endStream(true); } } + node_response.onRequestComplete(); } }, .success => {}, @@ -7734,6 +7745,10 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp if (!node_response.finished and node_response.response.state().isResponsePending()) { node_response.setOnAbortedHandler(); } + // If we ended the response without attaching an ondata handler, we discard the body read stream + else if (http_result != .pending) { + node_response.maybeStopReadingBody(vm); + } } } diff --git a/test/js/node/http/node-http.test.ts b/test/js/node/http/node-http.test.ts index 35569607481ac..814d2780f8c38 100644 --- a/test/js/node/http/node-http.test.ts +++ b/test/js/node/http/node-http.test.ts @@ -1875,10 +1875,11 @@ it("should emit events in the right order", async () => { ["req", "finish"], ["req", "response"], "STATUS: 200", + // TODO: not totally right: + ["req", "close"], ["res", "resume"], ["res", "readable"], ["res", "end"], - ["req", "close"], ["res", "close"], ]); }); From d9a1dc78f997969466d734dcdc492e1fc9f1e8de Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 7 Oct 2024 20:22:42 -0700 Subject: [PATCH 09/36] Fix some of this --- src/bun.js/api/server.zig | 6 +++++ src/bun.js/bindings/NodeHTTP.cpp | 20 +++++++++------ src/js/node/http.ts | 43 +++++++++++++++++++++++++------- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 5ed8dd2427c37..5b6fa0548c04a 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -6311,6 +6311,12 @@ pub const NodeHTTPResponse = struct { const bytes = string_or_buffer.slice(); + if (comptime is_end) { + log("end('{s}', {d})", .{ bytes[0..@min(bytes.len, 128)], bytes.len }); + } else { + log("write('{s}', {d})", .{ bytes[0..@min(bytes.len, 128)], bytes.len }); + } + if (is_end) { this.response.clearAborted(); this.response.clearOnWritable(); diff --git a/src/bun.js/bindings/NodeHTTP.cpp b/src/bun.js/bindings/NodeHTTP.cpp index 87b59886d9ecf..e7709185b2182 100644 --- a/src/bun.js/bindings/NodeHTTP.cpp +++ b/src/bun.js/bindings/NodeHTTP.cpp @@ -823,14 +823,15 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetHeader, (JSGlobalObject * globalObject, CallFr auto scope = DECLARE_THROW_SCOPE(vm); JSValue headersValue = callFrame->argument(0); + JSValue nameValue = callFrame->argument(1); + JSValue valueValue = callFrame->argument(2); if (auto* headers = jsDynamicCast(headersValue)) { - JSValue nameValue = callFrame->argument(1); + if (nameValue.isString()) { String name = nameValue.toWTFString(globalObject); FetchHeaders* impl = &headers->wrapped(); - JSValue valueValue = callFrame->argument(2); if (valueValue.isUndefined()) return JSValue::encode(jsUndefined()); @@ -841,23 +842,28 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetHeader, (JSGlobalObject * globalObject, CallFr JSValue item = array->getIndex(globalObject, 0); if (UNLIKELY(scope.exception())) return JSValue::encode(jsUndefined()); - impl->set(name, item.getString(globalObject)); + + auto value = item.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + impl->set(name, value); RETURN_IF_EXCEPTION(scope, {}); } for (unsigned i = 1; i < length; ++i) { JSValue value = array->getIndex(globalObject, i); if (UNLIKELY(scope.exception())) return JSValue::encode(jsUndefined()); - if (!value.isString()) - continue; - impl->append(name, value.getString(globalObject)); + auto string = value.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + impl->append(name, string); RETURN_IF_EXCEPTION(scope, {}); } RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined())); return JSValue::encode(jsUndefined()); } - impl->set(name, valueValue.getString(globalObject)); + auto value = valueValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + impl->set(name, value); RETURN_IF_EXCEPTION(scope, {}); return JSValue::encode(jsUndefined()); } diff --git a/src/js/node/http.ts b/src/js/node/http.ts index 324ea8f0b82a8..4e5261f24834e 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -466,8 +466,8 @@ function onRequestEvent(event) { server.emit("timeout", req.socket); break; case NodeHTTPResponseAbortEvent.abort: - this.complete = true; - this.emit("close"); + emitCloseNTAndComplete(http_res); + emitCloseNTAndComplete(this); http_res[finishedSymbol] = true; break; } @@ -968,7 +968,7 @@ function IncomingMessage(req, defaultIncomingOpts) { this._readableState.readingMore = true; } -IncomingMessage.prototype = { +const IncomingMessagePrototype = { constructor: IncomingMessage, __proto__: Readable.prototype, _construct(callback) { @@ -1165,6 +1165,7 @@ IncomingMessage.prototype = { this[fakeSocketSymbol] = value; }, } satisfies typeof import("node:http").IncomingMessage.prototype; +IncomingMessage.prototype = IncomingMessagePrototype; $setPrototypeDirect.$call(IncomingMessage, Readable); async function consumeStream(self, reader: ReadableStreamDefaultReader) { @@ -1356,7 +1357,9 @@ const OutgoingMessagePrototype = { } return this.write(data, encoding, callback); }, - end(chunk, encoding, callback) {}, + end(chunk, encoding, callback) { + return this; + }, } satisfies typeof import("node:http").OutgoingMessage.prototype; OutgoingMessage.prototype = OutgoingMessagePrototype; $setPrototypeDirect.$call(OutgoingMessage, Stream); @@ -1388,6 +1391,9 @@ function emitContinueAndSocketNT(self) { function emitCloseNT(self) { if (!self._closed) { self._closed = true; + if (!self.destroyed) { + self.destroy(); + } self.emit("close"); } } @@ -1540,6 +1546,12 @@ const ServerResponsePrototype = { handle.end(chunk, encoding); } } + const req = this.req; + const reqClosed = req?._closed; + if (reqClosed === false && req) { + process.nextTick(emitCloseNTAndComplete, req); + } + this[finishedSymbol] = this.finished = true; this.emit("prefinish"); @@ -1560,6 +1572,9 @@ const ServerResponsePrototype = { process.nextTick(function (self) { if (!self[closedSymbol]) { self[closedSymbol] = true; + if (!self.destroyed) { + self.destroy(); + } self.emit("close"); } }, self); @@ -1572,6 +1587,9 @@ const ServerResponsePrototype = { process.nextTick(function (self) { if (!self[closedSymbol]) { self[closedSymbol] = true; + if (!self.destroyed) { + self.destroy(); + } self.emit("close"); } }, this); @@ -1773,6 +1791,16 @@ const ServerResponsePrototype = { // throw new Error('not implemented'); }, + destroy(err?: Error) { + if (this.destroyed) return this; + const handle = this[kHandle]; + this.destroyed = true; + if (handle) { + handle.abort(); + } + return this; + }, + flushHeaders() { this._implicitHeader(); @@ -2045,9 +2073,10 @@ class ClientRequest extends (OutgoingMessage as unknown as typeof import("node:h } this.#send(); + return this; } - _destroy(err?: Error, callback) { + destroy(err?: Error, callback) { if (this.destroyed) return this; this.destroyed = true; @@ -2802,10 +2831,6 @@ function _writeHead(statusCode, reason, obj, response) { // consisting only of the Status-Line and optional headers, and is // terminated by an empty line. response._hasBody = false; - const req = response.req; - if (req) { - req.complete = true; - } } } From 8ca6dba52491ca3b9c3fdb8a6b3fd3f14146bdb8 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 7 Oct 2024 21:02:54 -0700 Subject: [PATCH 10/36] assign --- src/bun.js/bindings/ErrorCode.ts | 6 ++--- src/js/node/http.ts | 41 ++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index 52d3e081507c9..55e6640e03729 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -30,10 +30,10 @@ export default [ ["ERR_SERVER_NOT_RUNNING", Error, "Error"], ["ERR_SOCKET_BAD_TYPE", TypeError, "TypeError"], ["ERR_STREAM_ALREADY_FINISHED", TypeError, "TypeError"], - ["ERR_STREAM_CANNOT_PIPE", TypeError, "TypeError"], - ["ERR_STREAM_DESTROYED", TypeError, "TypeError"], + ["ERR_STREAM_CANNOT_PIPE", Error, "Error"], + ["ERR_STREAM_DESTROYED", Error, "Error"], ["ERR_STREAM_NULL_VALUES", TypeError, "TypeError"], - ["ERR_STREAM_WRITE_AFTER_END", TypeError, "TypeError"], + ["ERR_STREAM_WRITE_AFTER_END", Error, "Error"], ["ERR_ZLIB_INITIALIZATION_FAILED", Error, "Error"], ["ERR_STRING_TOO_LONG", Error, "Error"], ["ERR_CRYPTO_SCRYPT_INVALID_PARAMETER", Error, "Error"], diff --git a/src/js/node/http.ts b/src/js/node/http.ts index 4e5261f24834e..a42c646ee38be 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -1212,16 +1212,39 @@ const timeoutTimerSymbol = Symbol("timeoutTimer"); const fakeSocketSymbol = Symbol("fakeSocket"); function OutgoingMessage(options) { Stream.$call(this, options); - this.headersSent = false; + this.sendDate = true; this[finishedSymbol] = false; this[headerStateSymbol] = NodeHTTPHeaderState.none; this[kAbortController] = null; + + this.writable = true; + this.destroyed = false; + this._hasBody = true; + this._trailer = ""; + this._contentLength = null; + this._closed = false; + this._header = null; + this.finished = false; + this._headerSent = false; } const OutgoingMessagePrototype = { constructor: OutgoingMessage, __proto__: Stream.prototype, + // These are fields which we do not use in our implementation, but are observable in Node.js. + _keepAliveTimeout: 0, + _defaultKeepAlive: true, + shouldKeepAlive: true, + _onPendingData: function nop() {}, + outputSize: 0, + outputData: [], + strictContentLength: false, + _removedTE: false, + _removedContLen: false, + _removedConnection: false, + usesChunkedEncodingByDefault: true, + appendHeader(name, value) { var headers = (this[headersSymbol] ??= new Headers()); headers.append(name, value); @@ -1311,14 +1334,6 @@ const OutgoingMessagePrototype = { this[fakeSocketSymbol] = value; }, - get usesChunkedEncodingByDefault() { - return true; - }, - - set usesChunkedEncodingByDefault(value) { - // noop - }, - get chunkedEncoding() { return false; }, @@ -1459,10 +1474,6 @@ function ServerResponse(req, options) { this.statusCode = 200; this.statusMessage = undefined; this._sent100 = false; - this._defaultKeepAlive = false; - this._removedConnection = false; - this._removedContLen = false; - this._hasBody = true; this[headerStateSymbol] = NodeHTTPHeaderState.none; this[finishedSymbol] = false; @@ -1490,6 +1501,10 @@ const ServerResponsePrototype = { constructor: ServerResponse, __proto__: OutgoingMessage.prototype, + // Unused but observable fields: + _removedConnection: false, + _removedContLen: false, + get headersSent() { return this[headerStateSymbol] === NodeHTTPHeaderState.sent; }, From 3852109ea0a1077633adc729204dd52dda3768ab Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 7 Oct 2024 21:07:23 -0700 Subject: [PATCH 11/36] Update server.zig --- src/bun.js/api/server.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 5b6fa0548c04a..1c52d52c3a963 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -6193,11 +6193,13 @@ pub const NodeHTTPResponse = struct { this.body_read_state = .done; } + const was_finished = this.finished; + defer { if (last) { if (this.body_read_ref.has) { this.body_read_ref.unref(JSC.VirtualMachine.get()); - if (this.finished) { + if (this.finished and !was_finished) { this.server.onRequestComplete(); } this.deref(); From 2de2cb607bae14f69b781b3289a8819e624b8fdd Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 7 Oct 2024 22:48:29 -0700 Subject: [PATCH 12/36] wip --- src/bun.js/api/server.zig | 65 ++++++++--- src/js/node/http.ts | 237 +++++++++++++++++--------------------- 2 files changed, 157 insertions(+), 145 deletions(-) diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 1c52d52c3a963..1ae55071693e8 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -5808,6 +5808,7 @@ pub const NodeHTTPResponse = struct { aborted: bool = false, finished: bool = false, ended: bool = false, + is_request_pending: bool = true, body_read_state: BodyReadState = .none, body_read_ref: JSC.Ref = .{}, promise: JSC.Strong = .{}, @@ -5824,13 +5825,46 @@ pub const NodeHTTPResponse = struct { }; pub fn maybeStopReadingBody(this: *NodeHTTPResponse, vm: *JSC.VirtualMachine) void { - if (this.finished and this.ended and this.body_read_ref.has and this.body_read_state == .pending and !this.onDataCallback.has()) { + if ((this.aborted or this.ended) and (this.body_read_ref.has or this.body_read_state == .pending) and !this.onDataCallback.has()) { + const had_ref = this.body_read_ref.has; this.response.clearOnData(); this.body_read_ref.unref(vm); this.body_read_state = .done; - const server = this.server; + + if (had_ref) { + this.markRequestAsDoneIfNecessary(); + } + this.deref(); - server.onRequestComplete(); + } + } + + pub fn shouldRequestBePending(this: *const NodeHTTPResponse) bool { + if (this.aborted) { + return false; + } + + if (this.ended) { + return this.body_read_state == .pending; + } + + return true; + } + + fn markRequestAsDone(this: *NodeHTTPResponse) void { + log("markRequestAsDone()", .{}); + this.is_request_pending = false; + + this.clearJSValues(); + this.clearOnDataCallback(); + const server = this.server; + this.deref(); + server.onRequestComplete(); + } + + fn markRequestAsDoneIfNecessary(this: *NodeHTTPResponse) void { + if (this.is_request_pending and !this.shouldRequestBePending()) { + this.markRequestAsDone(); } } @@ -6048,12 +6082,12 @@ pub const NodeHTTPResponse = struct { return; } - defer if (event == .abort) this.onRequestComplete(); if (event == .abort) { this.aborted = true; } this.ref(); + defer if (event == .abort) this.markRequestAsDoneIfNecessary(); defer this.deref(); const js_this: JSValue = brk: { @@ -6101,11 +6135,8 @@ pub const NodeHTTPResponse = struct { this.finished = true; this.js_ref.unref(JSC.VirtualMachine.get()); - const server = this.server; this.clearJSValues(); - if (this.body_read_state != .pending) { - server.onRequestComplete(); - } + this.markRequestAsDoneIfNecessary(); this.deref(); } @@ -6194,14 +6225,13 @@ pub const NodeHTTPResponse = struct { } const was_finished = this.finished; + _ = was_finished; // autofix defer { if (last) { if (this.body_read_ref.has) { this.body_read_ref.unref(JSC.VirtualMachine.get()); - if (this.finished and !was_finished) { - this.server.onRequestComplete(); - } + this.markRequestAsDoneIfNecessary(); this.deref(); } @@ -6385,10 +6415,13 @@ pub const NodeHTTPResponse = struct { fn clearOnDataCallback(this: *NodeHTTPResponse) void { if (this.body_read_state != .none) { this.onDataCallback.deinit(); - this.response.clearOnData(); + if (!this.aborted) + this.response.clearOnData(); if (this.body_read_state != .done) { this.body_read_state = .done; - this.deref(); + if (this.body_read_ref.has) { + this.deref(); + } } } } @@ -6496,6 +6529,11 @@ pub const NodeHTTPResponse = struct { } pub fn deinit(this: *NodeHTTPResponse) void { + bun.debugAssert(!this.body_read_ref.has); + bun.debugAssert(!this.js_ref.has); + bun.debugAssert(!this.is_request_pending); + bun.debugAssert(this.aborted or this.finished); + this.js_ref.unref(JSC.VirtualMachine.get()); this.body_read_ref.unref(JSC.VirtualMachine.get()); this.onAbortedCallback.deinit(); @@ -6503,7 +6541,6 @@ pub const NodeHTTPResponse = struct { this.onWritableCallback.deinit(); this.strong_this.deinit(); this.promise.deinit(); - this.destroy(); } diff --git a/src/js/node/http.ts b/src/js/node/http.ts index a42c646ee38be..45f7f81cdea84 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -11,8 +11,8 @@ const enum NodeHTTPResponseAbortEvent { timeout = 2, } const enum NodeHTTPIncomingRequestType { - request, - response, + FetchRequest, + FetchResponse, NodeHTTPResponse, } const enum NodeHTTPHeaderState { @@ -25,6 +25,37 @@ const headerStateSymbol = Symbol("headerState"); // used for pretending to emit events in the right order const kEmitState = Symbol("emitState"); +const abortedSymbol = Symbol("aborted"); +const bodyStreamSymbol = Symbol("bodyStream"); +const closedSymbol = Symbol("closed"); +const controllerSymbol = Symbol("controller"); +const deferredSymbol = Symbol("deferred"); +const eofInProgress = Symbol("eofInProgress"); +const fakeSocketSymbol = Symbol("fakeSocket"); +const finishedSymbol = "finished"; +const firstWriteSymbol = Symbol("firstWrite"); +const headersSymbol = Symbol("headers"); +const isTlsSymbol = Symbol("is_tls"); +const kClearTimeout = Symbol("kClearTimeout"); +const kDeprecatedReplySymbol = Symbol("deprecatedReply"); +const kfakeSocket = Symbol("kfakeSocket"); +const kHandle = Symbol("handle"); +const kRealListen = Symbol("kRealListen"); +const noBodySymbol = Symbol("noBody"); +const optionsSymbol = Symbol("options"); +const reqSymbol = Symbol("req"); +const timeoutTimerSymbol = Symbol("timeoutTimer"); +const tlsSymbol = Symbol("tls"); +const typeSymbol = Symbol("type"); +const webRequestOrResponse = Symbol("FetchAPI"); +const statusCodeSymbol = Symbol("statusCode"); +const kEndCalled = Symbol.for("kEndCalled"); +const kAbortController = Symbol.for("kAbortController"); +const statusMessageSymbol = Symbol("statusMessage"); +const kInternalSocketData = Symbol.for("::bunternal::"); +const serverSymbol = Symbol.for("::bunternal::"); + +const kEmptyObject = Object.freeze(Object.create(null)); const EventEmitter: typeof import("node:events").EventEmitter = require("node:events"); const { isTypedArray } = require("node:util/types"); const { Duplex, Readable, Stream } = require("node:stream"); @@ -118,12 +149,6 @@ const setTimeout = globalThis.setTimeout; const fetch = Bun.fetch; const nop = () => {}; -const kEmptyObject = Object.freeze(Object.create(null)); -const kEndCalled = Symbol.for("kEndCalled"); -const kAbortController = Symbol.for("kAbortController"); -const kClearTimeout = Symbol("kClearTimeout"); -const kRealListen = Symbol("kRealListen"); - // Primordials const StringPrototypeSlice = String.prototype.slice; const StringPrototypeStartsWith = String.prototype.startsWith; @@ -135,11 +160,6 @@ const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/; const NODE_HTTP_WARNING = "WARN: Agent is mostly unused in Bun's implementation of http. If you see strange behavior, this is probably the cause."; -var kInternalRequest = Symbol("kInternalRequest"); -const kInternalSocketData = Symbol.for("::bunternal::"); -const serverSymbol = Symbol.for("::bunternal::"); -const kfakeSocket = Symbol("kfakeSocket"); - const kEmptyBuffer = Buffer.alloc(0); function isValidTLSArray(obj) { @@ -373,10 +393,6 @@ function emitListeningNextTick(self, hostname, port) { } } -var tlsSymbol = Symbol("tls"); -var isTlsSymbol = Symbol("is_tls"); -var optionsSymbol = Symbol("options"); - function Server(options, callback) { if (!(this instanceof Server)) return new Server(options, callback); EventEmitter.$call(this); @@ -678,15 +694,7 @@ Server.prototype = { const prevIsNextIncomingMessageHTTPS = isNextIncomingMessageHTTPS; isNextIncomingMessageHTTPS = isHTTPS; - const http_req = new RequestClass( - kInternalRequest, - url, - method, - headersObject, - headersArray, - handle, - hasBody, - ); + const http_req = new RequestClass(kHandle, url, method, headersObject, headersArray, handle, hasBody); const http_res = new ResponseClass(http_req, { [kHandle]: handle, }); @@ -764,7 +772,9 @@ Server.prototype = { const prevIsNextIncomingMessageHTTPS = isNextIncomingMessageHTTPS; isNextIncomingMessageHTTPS = isHTTPS; - const http_req = new RequestClass(req); + const http_req = new RequestClass(req, { + [typeSymbol]: NodeHTTPIncomingRequestType.FetchRequest, + }); assignEventCallback(req, onRequestEvent.bind(http_req)); isNextIncomingMessageHTTPS = prevIsNextIncomingMessageHTTPS; @@ -893,13 +903,6 @@ function requestHasNoBody(method, req) { // This lets us skip some URL parsing var isNextIncomingMessageHTTPS = false; -var typeSymbol = Symbol("type"); -var reqSymbol = Symbol("req"); -var bodyStreamSymbol = Symbol("bodyStream"); -var noBodySymbol = Symbol("noBody"); -var abortedSymbol = Symbol("aborted"); -const isInternalRequestSymbol = Symbol("isInternalRequest"); - function emitEOFIncomingMessageOuter(self) { self.push(null); self.complete = true; @@ -910,8 +913,6 @@ function emitEOFIncomingMessage(self) { process.nextTick(emitEOFIncomingMessageOuter, self); } -const eofInProgress = Symbol("eofInProgress"); - function IncomingMessage(req, defaultIncomingOpts) { this[abortedSymbol] = false; this[eofInProgress] = false; @@ -930,28 +931,26 @@ function IncomingMessage(req, defaultIncomingOpts) { } // (url, method, headers, rawHeaders, handle, hasBody) - if (req === kInternalRequest) { + if (req === kHandle) { this[typeSymbol] = NodeHTTPIncomingRequestType.NodeHTTPResponse; this.url = arguments[1]; this.method = arguments[2]; this.headers = arguments[3]; this.rawHeaders = arguments[4]; - this[kInternalRequest] = arguments[5]; + this[kHandle] = arguments[5]; this[noBodySymbol] = !arguments[6]; Readable.$call(this); } else { - this.method = null; this[noBodySymbol] = false; Readable.$call(this); - var { [typeSymbol]: type = NodeHTTPIncomingRequestType.request, [kInternalRequest]: nodeReq } = + var { [typeSymbol]: type = NodeHTTPIncomingRequestType.FetchRequest, [reqSymbol]: nodeReq } = defaultIncomingOpts || {}; - this[reqSymbol] = req; + this[webRequestOrResponse] = req; this[typeSymbol] = type; - this[bodyStreamSymbol] = undefined; - - this.req = nodeReq; + this[statusMessageSymbol] = (req as Response)?.statusText || null; + this[statusCodeSymbol] = (req as Response)?.status || 200; if (!assignHeaders(this, req)) { this[fakeSocketSymbol] = req; @@ -960,7 +959,7 @@ function IncomingMessage(req, defaultIncomingOpts) { } this[noBodySymbol] = - type === NodeHTTPIncomingRequestType.request // TODO: Add logic for checking for body on response + type === NodeHTTPIncomingRequestType.FetchRequest // TODO: Add logic for checking for body on response ? requestHasNoBody(this.method, this) : false; } @@ -975,8 +974,8 @@ const IncomingMessagePrototype = { // TODO: streaming const type = this[typeSymbol]; - if (type === NodeHTTPIncomingRequestType.response) { - if (!webRequestOrResponseHasBodyValue(this[reqSymbol])) { + if (type === NodeHTTPIncomingRequestType.FetchResponse) { + if (!webRequestOrResponseHasBodyValue(this[webRequestOrResponse])) { this.complete = true; this.push(null); } @@ -1010,7 +1009,7 @@ const IncomingMessagePrototype = { if (this[noBodySymbol]) { emitEOFIncomingMessage(this); return; - } else if ((internalRequest = this[kInternalRequest])) { + } else if ((internalRequest = this[kHandle])) { internalRequest.ondata = (chunk, isLast, aborted: NodeHTTPResponseAbortEvent) => { $debug("ondata", chunk, isLast, aborted); if (aborted === NodeHTTPResponseAbortEvent.abort) { @@ -1032,7 +1031,7 @@ const IncomingMessagePrototype = { return true; } else if (this[bodyStreamSymbol] == null) { // If it's all available right now, we skip going through ReadableStream. - let completeBody = getCompleteWebRequestOrResponseBodyValueAsArrayBuffer(this[reqSymbol]); + let completeBody = getCompleteWebRequestOrResponseBodyValueAsArrayBuffer(this[webRequestOrResponse]); if (completeBody) { $assert(completeBody instanceof ArrayBuffer, "completeBody is not an ArrayBuffer"); $assert(completeBody.byteLength > 0, "completeBody should not be empty"); @@ -1045,7 +1044,7 @@ const IncomingMessagePrototype = { return; } - const reader = this[reqSymbol].body?.getReader?.() as ReadableStreamDefaultReader; + const reader = this[webRequestOrResponse].body?.getReader?.() as ReadableStreamDefaultReader; if (!reader) { emitEOFIncomingMessage(this); return; @@ -1073,10 +1072,11 @@ const IncomingMessagePrototype = { err = undefined; } - var internalRequest = this[kInternalRequest]; - if (internalRequest) { - this[kInternalRequest] = undefined; - internalRequest.ondata = undefined; + var nodeHTTPResponse = this[kHandle]; + if (nodeHTTPResponse) { + this[kHandle] = undefined; + nodeHTTPResponse.ondata = undefined; + nodeHTTPResponse.abort(); } else { const stream = this[bodyStreamSymbol]; this[bodyStreamSymbol] = undefined; @@ -1106,17 +1106,17 @@ const IncomingMessagePrototype = { return (this[fakeSocketSymbol] ??= new FakeSocket()); }, get statusCode() { - return this[reqSymbol].status; + return this[statusCodeSymbol]; }, set statusCode(value) { if (!(value in STATUS_CODES)) return; - this[reqSymbol].status = value; + this[statusCodeSymbol] = value; }, get statusMessage() { - return STATUS_CODES[this[reqSymbol].status]; + return this[statusMessageSymbol]; }, set statusMessage(value) { - // noop + this[statusMessageSymbol] = value; }, get httpVersion() { return "1.1"; @@ -1150,7 +1150,7 @@ const IncomingMessagePrototype = { }, setTimeout(msecs, callback) { this.take; - const req = this[reqSymbol] || this[kInternalRequest]; + const req = this[kHandle] || this[webRequestOrResponse]; if (req) { setRequestTimeout(req, Math.ceil(msecs / 1000)); @@ -1206,11 +1206,11 @@ async function consumeStream(self, reader: ReadableStreamDefaultReader) { } } -const headersSymbol = Symbol("headers"); -const finishedSymbol = Symbol("finished"); -const timeoutTimerSymbol = Symbol("timeoutTimer"); -const fakeSocketSymbol = Symbol("fakeSocket"); function OutgoingMessage(options) { + if (!new.target) { + return new OutgoingMessage(options); + } + Stream.$call(this, options); this.sendDate = true; @@ -1225,7 +1225,6 @@ function OutgoingMessage(options) { this._contentLength = null; this._closed = false; this._header = null; - this.finished = false; this._headerSent = false; } const OutgoingMessagePrototype = { @@ -1258,41 +1257,53 @@ const OutgoingMessagePrototype = { getHeader(name) { return getHeader(this[headersSymbol], name); }, - getHeaders() { - if (!this[headersSymbol]) return kEmptyObject; - return this[headersSymbol].toJSON(); - }, + getHeaderNames() { var headers = this[headersSymbol]; if (!headers) return []; return Array.from(headers.keys()); }, + + getHeaders() { + const headers = this[headersSymbol]; + if (!headers) return kEmptyObject; + return headers.toJSON(); + }, + removeHeader(name) { - if (!this[headersSymbol]) return; - this[headersSymbol].delete(name); + if (this[headerStateSymbol] === NodeHTTPHeaderState.sent) { + throw $ERR_HTTP_HEADERS_SENT("Cannot remove header after headers have been sent."); + } + const headers = this[headersSymbol]; + if (!headers) return; + headers.delete(name); }, + setHeader(name, value) { - this[headersSymbol] = this[headersSymbol] ?? new Headers(); - var headers = this[headersSymbol]; - headers.set(name, value); + const headers = (this[headersSymbol] ??= new Headers()); + setHeader(headers, name, value); return this; }, + hasHeader(name) { - if (!this[headersSymbol]) return false; - return this[headersSymbol].has(name); - }, - addTrailers(headers) { - throw new Error("not implemented"); + const headers = this[headersSymbol]; + if (!headers) return false; + return headers.has(name); }, get headers() { - if (!this[headersSymbol]) return kEmptyObject; - return this[headersSymbol].toJSON(); + const headers = this[headersSymbol]; + if (!headers) return kEmptyObject; + return headers.toJSON(); }, set headers(value) { this[headersSymbol] = new Headers(value); }, + addTrailers(headers) { + throw new Error("not implemented"); + }, + setTimeout(msecs, callback) { if (this.destroyed) return this; @@ -1375,6 +1386,15 @@ const OutgoingMessagePrototype = { end(chunk, encoding, callback) { return this; }, + destroy(err?: Error) { + if (this.destroyed) return this; + const handle = this[kHandle]; + this.destroyed = true; + if (handle) { + handle.abort(); + } + return this; + }, } satisfies typeof import("node:http").OutgoingMessage.prototype; OutgoingMessage.prototype = OutgoingMessagePrototype; $setPrototypeDirect.$call(OutgoingMessage, Stream); @@ -1451,14 +1471,12 @@ function onServerResponseClose() { } let OriginalWriteHeadFn, OriginalImplicitHeadFn; -const controllerSymbol = Symbol("controller"); -const firstWriteSymbol = Symbol("firstWrite"); -const deferredSymbol = Symbol("deferred"); -const kDeprecatedReplySymbol = Symbol("deprecatedReply"); -const kHandle = Symbol("handle"); -const closedSymbol = Symbol("closed"); function ServerResponse(req, options) { + if (!new.target) { + return new ServerResponse(req, options); + } + if ((this[kDeprecatedReplySymbol] = options?.[kDeprecatedReplySymbol])) { this[controllerSymbol] = undefined; this[firstWriteSymbol] = undefined; @@ -1471,11 +1489,8 @@ function ServerResponse(req, options) { this.req = req; this.sendDate = true; - this.statusCode = 200; - this.statusMessage = undefined; this._sent100 = false; this[headerStateSymbol] = NodeHTTPHeaderState.none; - this[finishedSymbol] = false; // this is matching node's behaviour // https://github.com/nodejs/node/blob/cf8c6994e0f764af02da4fa70bc5962142181bf3/lib/_http_server.js#L192 @@ -1676,46 +1691,6 @@ const ServerResponsePrototype = { return true; }, - appendHeader(name, value) { - const headers = (this[headersSymbol] ??= new Headers()); - headers.append(name, value); - - return this; - }, - - getHeader(name) { - return getHeader(this[headersSymbol], name); - }, - - getHeaders() { - const headers = this[headersSymbol]; - if (!headers) return kEmptyObject; - return headers.toJSON(); - }, - - getHeaderNames() { - const headers = this[headersSymbol]; - if (!headers) return []; - return Array.from(headers.keys()); - }, - - removeHeader(name) { - if (!this[headersSymbol]) return; - this[headersSymbol].delete(name); - }, - - setHeader(name, value) { - this[headersSymbol] = this[headersSymbol] ?? new Headers(); - const headers = this[headersSymbol]; - setHeader(headers, name, value); - return this; - }, - - hasHeader(name) { - if (!this[headersSymbol]) return false; - return this[headersSymbol].has(name); - }, - _finish() { OutgoingMessage.prototype._finish.$call(this); }, @@ -2201,8 +2176,8 @@ class ClientRequest extends (OutgoingMessage as unknown as typeof import("node:h const prevIsHTTPS = isNextIncomingMessageHTTPS; isNextIncomingMessageHTTPS = response.url.startsWith("https:"); var res = (this.#res = new IncomingMessage(response, { - [typeSymbol]: NodeHTTPIncomingRequestType.response, - [kInternalRequest]: this, + [typeSymbol]: NodeHTTPIncomingRequestType.FetchResponse, + [reqSymbol]: this, })); isNextIncomingMessageHTTPS = prevIsHTTPS; res.req = this; From da3e7160a160be882da58b3f835b105fe5d618bf Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 7 Oct 2024 22:48:55 -0700 Subject: [PATCH 13/36] Test --- test/regression/issue/04298/04298.fixture.js | 6 ++++-- test/regression/issue/04298/04298.test.ts | 5 ++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/test/regression/issue/04298/04298.fixture.js b/test/regression/issue/04298/04298.fixture.js index 77161560d6f82..6c8374ce42ca7 100644 --- a/test/regression/issue/04298/04298.fixture.js +++ b/test/regression/issue/04298/04298.fixture.js @@ -5,11 +5,13 @@ const server = createServer((req, res) => { throw new Error("Oops!"); }); -server.listen({ port: 0 }, async (err, host, port) => { +server.listen({ port: 0 }, async err => { + const { port, address: host } = server.address(); if (err) { console.error(err); process.exit(1); } const hostname = isIPv6(host) ? `[${host}]` : host; - process.send(`http://${hostname}:${port}/`); + + (process?.connected ? process.send : console.log)(`http://${hostname}:${port}/`); }); diff --git a/test/regression/issue/04298/04298.test.ts b/test/regression/issue/04298/04298.test.ts index ea41f5bcce486..d4def8502863a 100644 --- a/test/regression/issue/04298/04298.test.ts +++ b/test/regression/issue/04298/04298.test.ts @@ -2,13 +2,13 @@ import { spawn } from "bun"; import { expect, test } from "bun:test"; import { bunEnv, bunExe } from "harness"; -test("node:http should not crash when server throws", async () => { +test("node:http should not crash when server throws, and should abruptly close the socket", async () => { const { promise, resolve, reject } = Promise.withResolvers(); await using server = spawn({ cwd: import.meta.dirname, cmd: [bunExe(), "04298.fixture.js"], env: bunEnv, - stderr: "pipe", + stderr: "inherit", ipc(url) { resolve(url); }, @@ -20,5 +20,4 @@ test("node:http should not crash when server throws", async () => { }); const url = await promise; const response = await fetch(url); - expect(response.status).toBe(500); }); From 00e017c2743b075be57a9d9d5eeecfbfb09045df Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Wed, 9 Oct 2024 02:33:53 -0700 Subject: [PATCH 14/36] stash this --- packages/bun-uws/src/HttpContext.h | 3 +- packages/bun-uws/src/HttpContextData.h | 5 + packages/bun-uws/src/HttpResponse.h | 11 ++ packages/bun-uws/src/HttpResponseData.h | 3 + src/bun.js/bindings/NodeHTTP.cpp | 135 ++++++++++++++++++ src/bun.js/bindings/NodeHTTP.h | 5 +- src/bun.js/bindings/ZigGlobalObject.cpp | 6 +- src/bun.js/bindings/ZigGlobalObject.h | 2 + .../bindings/webcore/DOMClientIsoSubspaces.h | 1 + src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 3 +- 10 files changed, 169 insertions(+), 5 deletions(-) diff --git a/packages/bun-uws/src/HttpContext.h b/packages/bun-uws/src/HttpContext.h index 338683f816890..efee5c3176b30 100644 --- a/packages/bun-uws/src/HttpContext.h +++ b/packages/bun-uws/src/HttpContext.h @@ -127,8 +127,9 @@ struct HttpContext { /* Signal broken HTTP request only if we have a pending request */ if (httpResponseData->onAborted) { httpResponseData->onAborted((HttpResponse *)s, httpResponseData->userData); + } else if (httpResponseData->socketData && httpContextData->onSocketClosed) { + httpContextData->onSocketClosed(httpResponseData->socketData, SSL, s); } - /* Destruct socket ext */ httpResponseData->~HttpResponseData(); diff --git a/packages/bun-uws/src/HttpContextData.h b/packages/bun-uws/src/HttpContextData.h index 502941de8746a..53f1b91065b8e 100644 --- a/packages/bun-uws/src/HttpContextData.h +++ b/packages/bun-uws/src/HttpContextData.h @@ -27,6 +27,7 @@ namespace uWS { template struct HttpResponse; struct HttpRequest; + template struct alignas(16) HttpContextData { template friend struct HttpContext; @@ -34,6 +35,7 @@ struct alignas(16) HttpContextData { template friend struct TemplatedApp; private: std::vector *, int)>> filterHandlers; + using OnSocketClosedCallback = void (*)(void* userData, int is_ssl, struct us_socket_t *rawSocket); MoveOnlyFunction missingServerNameHandler; @@ -51,6 +53,9 @@ struct alignas(16) HttpContextData { bool isParsingHttp = false; bool rejectUnauthorized = false; + /* Used to simulate Node.js socket events. */ + OnSocketClosedCallback onSocketClosed = nullptr; + // TODO: SNI void clearRoutes() { this->router = HttpRouter{}; diff --git a/packages/bun-uws/src/HttpResponse.h b/packages/bun-uws/src/HttpResponse.h index 5c5f56cd85869..9f93dd67b3831 100644 --- a/packages/bun-uws/src/HttpResponse.h +++ b/packages/bun-uws/src/HttpResponse.h @@ -647,6 +647,17 @@ struct HttpResponse : public AsyncSocket { data->received_bytes_per_timeout = 0; } + void* getSocketData() { + HttpResponseData *httpResponseData = getHttpResponseData(); + + return httpResponseData->socketData; + } + + void setSocketData(void* socketData) { + HttpResponseData *httpResponseData = getHttpResponseData(); + + httpResponseData->socketData = socketData; + } void setWriteOffset(uint64_t offset) { HttpResponseData *httpResponseData = getHttpResponseData(); diff --git a/packages/bun-uws/src/HttpResponseData.h b/packages/bun-uws/src/HttpResponseData.h index 3b07b7257bb1c..d118534a2d51b 100644 --- a/packages/bun-uws/src/HttpResponseData.h +++ b/packages/bun-uws/src/HttpResponseData.h @@ -85,6 +85,7 @@ struct HttpResponseData : AsyncSocketData, HttpParser { /* Shared context pointer */ void* userData = nullptr; + void* socketData = nullptr; /* Per socket event handlers */ OnWritableCallback onWritable = nullptr; @@ -109,3 +110,5 @@ struct HttpResponseData : AsyncSocketData, HttpParser { } #endif // UWS_HTTPRESPONSEDATA_H + +static_assert(sizeof(uWS::HttpResponseData) == 128, "HttpResponseData size is incorrect"); \ No newline at end of file diff --git a/src/bun.js/bindings/NodeHTTP.cpp b/src/bun.js/bindings/NodeHTTP.cpp index e7709185b2182..ea44662172fdc 100644 --- a/src/bun.js/bindings/NodeHTTP.cpp +++ b/src/bun.js/bindings/NodeHTTP.cpp @@ -21,6 +21,141 @@ namespace Bun { using namespace JSC; using namespace WebCore; +// Create a static hash table of values containing an onclose DOMAttributeGetterSetter and a close function +static const struct HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] = { + { "onclose"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterOnClose, setterOnClose } }, + { "close"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, functionNodeHTTPServerSocket_close, 0 } }, +}; + +class JSNodeHTTPServerSocketPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + + static JSNodeHTTPServerSocketPrototype* create(VM& vm, JSGlobalObject* globalObject, Structure* structure) + { + JSNodeHTTPServerSocketPrototype* prototype = new (NotNull, allocateCell(vm)) JSNodeHTTPServerSocketPrototype(vm, structure); + prototype->finishCreation(vm, globalObject); + return prototype; + } + + DECLARE_INFO; + + static constexpr bool needsDestruction = false; + static constexpr unsigned StructureFlags = Base::StructureFlags | HasStaticPropertyTable; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSNodeHTTPServerSocketPrototype, Base); + return &vm.plainObjectSpace(); + } + +private: + JSNodeHTTPServerSocketPrototype(VM& vm, Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(VM& vm, JSGlobalObject* globalObject) + { + Base::finishCreation(vm); + ASSERT(inherits(info())); + } +}; + +class JSNodeHTTPServerSocket : public JSC::JSDestructibleObject { + using Base = JSC::JSDestructibleObject; + +public: + static JSNodeHTTPServerSocket* create(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl) + { + return new (JSC::allocateCell(vm)) JSNodeHTTPServerSocket(vm, structure, socket, is_ssl); + } + + static JSNodeHTTPServerSocket* create(JSC::VM& vm, Zig::GlobalObject* globalObject, us_socket_t* socket, bool is_ssl) + { + auto* structure = globalObject->m_JSNodeHTTPServerSocketStructure.getInitializedOnMainThread(globalObject); + return create(vm, structure, socket, is_ssl); + } + + static void destroy(JSC::JSCell* cell) + { + static_cast(cell)->JSNodeHTTPServerSocket::~JSNodeHTTPServerSocket(); + } + + template + static void clearSocketData(us_socket_t* socket) + { + auto* httpResponseData = (uWS::HttpResponseData*)us_socket_ext(SSL, socket); + if (httpResponseData->socketData) { + httpResponseData->socketData = nullptr; + } + } + + ~JSNodeHTTPServerSocket() + { + if (socket) { + if (is_ssl) { + clearSocketData(socket); + } else { + clearSocketData(socket); + } + } + } + + JSNodeHTTPServerSocket(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl) + : JSC::JSDestructibleObject(vm, structure) + , socket(socket) + , is_ssl(is_ssl) + { + } + + mutable WriteBarrier functionToCallOnClose; + mutable WriteBarrier currentResponseObject; + unsigned is_ssl : 1; + us_socket_t* socket; + + DECLARE_INFO; + DECLARE_VISIT_CHILDREN; + + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForJSNodeHTTPServerSocket.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSNodeHTTPServerSocket = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForJSNodeHTTPServerSocket.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForJSNodeHTTPServerSocket = std::forward(space); }); + } + + void onClose() + { + this->socket = nullptr; + } + + static JSC_HOST_CALL_ATTRIBUTES JSC::EncodedJSValue onClose(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) + { + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (!thisObject) { + return JSValue::encode(JSC::jsUndefined()); + } + + thisObject->onClose(); + return JSValue::encode(JSC::jsUndefined()); + } + + static Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) + { + JSC::JSObject* prototype = JSC::constructEmptyObject(vm, globalObject->nullPrototypeObjectStructure()); + prototype->structure()->setMayBePrototype(true); + + return JSC::Structure::create(vm, globalObject, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } +}; + BUN_DECLARE_HOST_FUNCTION(jsFunctionRequestOrResponseHasBodyValue); BUN_DECLARE_HOST_FUNCTION(jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer); extern "C" uWS::HttpRequest* Request__getUWSRequest(void*); diff --git a/src/bun.js/bindings/NodeHTTP.h b/src/bun.js/bindings/NodeHTTP.h index e79a2b21d1670..b682d826b2e11 100644 --- a/src/bun.js/bindings/NodeHTTP.h +++ b/src/bun.js/bindings/NodeHTTP.h @@ -1,11 +1,12 @@ #include "config.h" namespace Bun { - + JSC_DECLARE_HOST_FUNCTION(jsHTTPAssignHeaders); JSC_DECLARE_HOST_FUNCTION(jsHTTPGetHeader); JSC_DECLARE_HOST_FUNCTION(jsHTTPSetHeader); +JSC::Structure* createNodeHTTPServerSocketStructure(JSC::VM& vm); JSC::JSValue createNodeHTTPInternalBinding(Zig::GlobalObject*); -} \ No newline at end of file +} diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index f8466406ea330..2039774239d22 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -2687,7 +2687,10 @@ void GlobalObject::finishCreation(VM& vm) m_commonStrings.initialize(); Bun::addNodeModuleConstructorProperties(vm, this); - + m_JSNodeHTTPServerSocketStructure.initLater( + [](const Initializer& init) { + init.set(Bun::createNodeHTTPServerSocketStructure(init.vm)); + }); m_JSDOMFileConstructor.initLater( [](const Initializer& init) { JSObject* fileConstructor = Bun::createJSDOMFileConstructor(init.vm, init.owner); @@ -3632,6 +3635,7 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->m_JSBufferClassStructure.visit(visitor); thisObject->m_JSBufferListClassStructure.visit(visitor); thisObject->m_JSBufferSubclassStructure.visit(visitor); + thisObject->m_JSNodeHTTPServerSocketStructure.visit(visitor); thisObject->m_JSCryptoKey.visit(visitor); thisObject->m_JSDOMFileConstructor.visit(visitor); thisObject->m_JSFFIFunctionStructure.visit(visitor); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index b80d29315baa7..3ad3b852aa64d 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -587,6 +587,8 @@ class GlobalObject : public Bun::GlobalScope { LazyProperty m_performanceObject; LazyProperty m_processObject; + LazyProperty m_JSNodeHTTPServerSocketStructure; + bool hasOverridenModuleResolveFilenameFunction = false; private: diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index 3ba6b4c6e2efc..d4f177c1ccade 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -56,6 +56,7 @@ class DOMClientIsoSubspaces { std::unique_ptr m_clientSubspaceForHandleScopeBuffer; std::unique_ptr m_clientSubspaceForFunctionTemplate; std::unique_ptr m_clientSubspaceForV8Function; + std::unique_ptr m_clientSubspaceForJSNodeHTTPServerSocket; #include "ZigGeneratedClasses+DOMClientIsoSubspaces.h" /* --- bun --- */ diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index e86f18bfd52e4..cee6e4a3fcf50 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -56,6 +56,7 @@ class DOMIsoSubspaces { std::unique_ptr m_subspaceForHandleScopeBuffer; std::unique_ptr m_subspaceForFunctionTemplate; std::unique_ptr m_subspaceForV8Function; + std::unique_ptr m_subspaceForJSNodeHTTPServerSocket; #include "ZigGeneratedClasses+DOMIsoSubspaces.h" /*-- BUN --*/ @@ -898,7 +899,7 @@ class DOMIsoSubspaces { // std::unique_ptr m_subspaceForXPathNSResolver; // std::unique_ptr m_subspaceForXPathResult; // std::unique_ptr m_subspaceForXSLTProcessor; - + std::unique_ptr m_subspaceForBakeGlobalScope; std::unique_ptr m_subspaceForAbortController; From 9593dcde986cde990f1f67265de0e3cc4ff0d529 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 10 Oct 2024 04:27:31 -0700 Subject: [PATCH 15/36] Make Next.js 15% faster --- packages/bun-uws/src/App.h | 4 + packages/bun-uws/src/HttpContext.h | 4 +- packages/bun-uws/src/HttpResponse.h | 4 + packages/bun-uws/src/HttpResponseData.h | 2 - src/bun.js/api/server.zig | 50 ++-- src/bun.js/bindings/BunObject.cpp | 27 ++ src/bun.js/bindings/CachedScript.h | 5 +- src/bun.js/bindings/JSSocketAddress.cpp | 36 ++- src/bun.js/bindings/JSSocketAddress.h | 3 +- src/bun.js/bindings/NodeHTTP.cpp | 381 +++++++++++++++++++++--- src/bun.js/bindings/NodeHTTP.h | 2 +- src/bun.js/bindings/ZigGlobalObject.cpp | 2 +- src/bun.js/javascript.zig | 6 + src/deps/uws.zig | 6 + src/js/node/http.ts | 222 ++++++++++++-- 15 files changed, 645 insertions(+), 109 deletions(-) diff --git a/packages/bun-uws/src/App.h b/packages/bun-uws/src/App.h index be91e146d6832..3495673012eb1 100644 --- a/packages/bun-uws/src/App.h +++ b/packages/bun-uws/src/App.h @@ -592,6 +592,10 @@ struct TemplatedApp { return std::move(*this); } + void setOnClose(HttpContextData::OnSocketClosedCallback onClose) { + httpContext->getSocketContextData()->onSocketClosed = onClose; + } + TemplatedApp &&run() { uWS::run(); return std::move(*this); diff --git a/packages/bun-uws/src/HttpContext.h b/packages/bun-uws/src/HttpContext.h index efee5c3176b30..f14c3570b2e1c 100644 --- a/packages/bun-uws/src/HttpContext.h +++ b/packages/bun-uws/src/HttpContext.h @@ -127,7 +127,9 @@ struct HttpContext { /* Signal broken HTTP request only if we have a pending request */ if (httpResponseData->onAborted) { httpResponseData->onAborted((HttpResponse *)s, httpResponseData->userData); - } else if (httpResponseData->socketData && httpContextData->onSocketClosed) { + } + + if (httpResponseData->socketData && httpContextData->onSocketClosed) { httpContextData->onSocketClosed(httpResponseData->socketData, SSL, s); } diff --git a/packages/bun-uws/src/HttpResponse.h b/packages/bun-uws/src/HttpResponse.h index 9f93dd67b3831..951d2b6e3335a 100644 --- a/packages/bun-uws/src/HttpResponse.h +++ b/packages/bun-uws/src/HttpResponse.h @@ -150,6 +150,8 @@ struct HttpResponse : public AsyncSocket { } } } + } else { + this->uncork(); } /* tryEnd can never fail when in chunked mode, since we do not have tryWrite (yet), only write */ @@ -213,6 +215,8 @@ struct HttpResponse : public AsyncSocket { } } } + } else { + this->uncork(); } } diff --git a/packages/bun-uws/src/HttpResponseData.h b/packages/bun-uws/src/HttpResponseData.h index d118534a2d51b..316430e562c9e 100644 --- a/packages/bun-uws/src/HttpResponseData.h +++ b/packages/bun-uws/src/HttpResponseData.h @@ -110,5 +110,3 @@ struct HttpResponseData : AsyncSocketData, HttpParser { } #endif // UWS_HTTPRESPONSEDATA_H - -static_assert(sizeof(uWS::HttpResponseData) == 128, "HttpResponseData size is incorrect"); \ No newline at end of file diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 1ff67824bfd46..83fd5dcecbdd1 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -5801,7 +5801,6 @@ pub const NodeHTTPResponse = struct { onDataCallback: JSC.Strong = .{}, onWritableCallback: JSC.Strong = .{}, onAbortedCallback: JSC.Strong = .{}, - strong_this: JSC.Strong = .{}, ref_count: u32 = 1, js_ref: JSC.Ref = .{}, @@ -5824,6 +5823,11 @@ pub const NodeHTTPResponse = struct { done, }; + extern "C" fn Bun__getNodeHTTPResponseThisValue(c_int, *anyopaque) JSC.JSValue; + fn getThisValue(this: *NodeHTTPResponse) JSC.JSValue { + return Bun__getNodeHTTPResponseThisValue(@intFromBool(this.response == .SSL), this.response.socket()); + } + pub fn maybeStopReadingBody(this: *NodeHTTPResponse, vm: *JSC.VirtualMachine) void { if ((this.aborted or this.ended) and (this.body_read_ref.has or this.body_read_state == .pending) and !this.onDataCallback.has()) { const had_ref = this.body_read_ref.has; @@ -5908,7 +5912,6 @@ pub const NodeHTTPResponse = struct { } response.js_ref.ref(vm); const js_this = response.toJS(globalObject); - response.strong_this.set(globalObject, js_this); node_response_ptr.* = response; return js_this; } @@ -5996,10 +5999,6 @@ pub const NodeHTTPResponse = struct { if (handleEndedIfNecessary(state, globalObject)) { return .zero; } - if (state.isHttpWriteCalled() or state.isHttpStatusCalled()) { - globalObject.ERR_HTTP_HEADERS_SENT("Stream already started", .{}).throw(); - return .zero; - } const status_code_value = if (arguments.len > 0) arguments[0] else .undefined; const status_message_value = if (arguments.len > 1 and arguments[1] != .null) arguments[1] else .undefined; @@ -6028,6 +6027,11 @@ pub const NodeHTTPResponse = struct { return .zero; } + if (state.isHttpStatusCalled()) { + globalObject.ERR_HTTP_HEADERS_SENT("Stream already started", .{}).throw(); + return .zero; + } + do_it: { if (status_message_slice.len == 0) { if (HTTPStatusText.get(@intCast(status_code))) |status_message| { @@ -6090,13 +6094,7 @@ pub const NodeHTTPResponse = struct { defer if (event == .abort) this.markRequestAsDoneIfNecessary(); defer this.deref(); - const js_this: JSValue = brk: { - if (comptime event == .abort) { - break :brk this.strong_this.trySwap() orelse .undefined; - } - break :brk this.strong_this.get() orelse .undefined; - }; - + const js_this: JSValue = this.getThisValue(); if (this.onAbortedCallback.get()) |on_aborted| { defer { if (event == .abort) { @@ -6137,7 +6135,6 @@ pub const NodeHTTPResponse = struct { this.clearJSValues(); this.markRequestAsDoneIfNecessary(); - this.deref(); } pub export fn Bun__NodeHTTPRequest__onResolve(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { @@ -6191,7 +6188,6 @@ pub const NodeHTTPResponse = struct { fn clearJSValues(this: *NodeHTTPResponse) void { // Promise is handled separately. - this.strong_this.deinit(); this.onWritableCallback.deinit(); this.onAbortedCallback.deinit(); } @@ -6249,6 +6245,7 @@ pub const NodeHTTPResponse = struct { }); } } + pub const BUN_DEBUG_REFCOUNT_NAME = "NodeHTTPServerResponse"; pub fn onData(this: *NodeHTTPResponse, chunk: []const u8, last: bool) void { log("onData({d} bytes, is_last = {d})", .{ chunk.len, @intFromBool(last) }); @@ -6539,7 +6536,6 @@ pub const NodeHTTPResponse = struct { this.onAbortedCallback.deinit(); this.onDataCallback.deinit(); this.onWritableCallback.deinit(); - this.strong_this.deinit(); this.promise.deinit(); this.destroy(); } @@ -7399,8 +7395,13 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp httplog("scheduleDeinit", .{}); if (!this.flags.terminated) { + // App.close can cause finalizers to run. + // scheduleDeinit can be called inside a finalizer. + // Therefore, we split it into two tasks. this.flags.terminated = true; - this.app.close(); + const task = bun.default_allocator.create(JSC.AnyTask) catch unreachable; + task.* = JSC.AnyTask.New(App, App.close).init(this.app); + this.vm.enqueueTask(JSC.Task.init(task)); } const task = bun.default_allocator.create(JSC.AnyTask) catch unreachable; @@ -7747,14 +7748,10 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp break :brk .{ .success = {} }; } - if (node_response.strong_this.get()) |strong_self| { - node_response.promise = strong_promise; - strong_promise = .{}; - result._then(globalThis, strong_self, NodeHTTPResponse.Bun__NodeHTTPRequest__onResolve, NodeHTTPResponse.Bun__NodeHTTPRequest__onReject); - } else { - @panic("This should not happen"); - } - + const strong_self = node_response.getThisValue(); + node_response.promise = strong_promise; + strong_promise = .{}; + result._then(globalThis, strong_self, NodeHTTPResponse.Bun__NodeHTTPRequest__onResolve, NodeHTTPResponse.Bun__NodeHTTPRequest__onReject); is_async = true; } @@ -8022,6 +8019,7 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp if (this.config.onNodeHTTPRequest != .zero) { this.app.any("/*", *ThisServer, this, onNodeHTTPRequest); + NodeHTTP_assignOnCloseFunction(@intFromBool(ssl_enabled), this.app); } else if (this.config.onRequest != .zero) { this.app.any("/*", *ThisServer, this, onRequest); } @@ -8261,3 +8259,5 @@ extern fn NodeHTTPServer__onRequest_https( response: *uws.NewApp(true).Response, node_response_ptr: *?*NodeHTTPResponse, ) JSC.JSValue; + +extern fn NodeHTTP_assignOnCloseFunction(c_int, *anyopaque) void; diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index 49f08a6ca81e2..48147ef93e66d 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -560,6 +560,32 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj return JSC::JSValue::encode(JSC::jsString(vm, fileSystemPath)); } +JSC_DEFINE_HOST_FUNCTION(functionFork, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + auto* globalObject = defaultGlobalObject(lexicalGlobalObject); + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + JSC::Strong strongCallback(vm, callFrame->argument(0).getObject()); + + globalObject->scriptExecutionContext()->postTask([strongCallback = WTFMove(strongCallback)](auto& context) { + auto pid = fork(); + if (pid > 0) { + waitpid(pid, nullptr, 0); + } + printf("post-fork%d\n", pid); + auto* function = strongCallback.get(); + auto* globalObject = context.globalObject(); + JSC::MarkedArgumentBuffer arguments; + arguments.append(jsNumber(pid)); + WTF::NakedPtr exception; + JSC::call(globalObject, function, JSC::getCallData(function), JSC::jsUndefined(), arguments, exception); + if (UNLIKELY(exception)) { + } + }); + + return JSValue::encode(jsUndefined()); +} + /* Source for BunObject.lut.h @begin bunObjectTable $ constructBunShell ReadOnly|DontDelete|PropertyCallback @@ -589,6 +615,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj deepEquals functionBunDeepEquals DontDelete|Function 2 deepMatch functionBunDeepMatch DontDelete|Function 2 deflateSync BunObject_callback_deflateSync DontDelete|Function 1 + fork functionFork DontDelete|Function 1 dns constructDNSObject ReadOnly|DontDelete|PropertyCallback enableANSIColors BunObject_getter_wrap_enableANSIColors DontDelete|PropertyCallback env constructEnvObject ReadOnly|DontDelete|PropertyCallback diff --git a/src/bun.js/bindings/CachedScript.h b/src/bun.js/bindings/CachedScript.h index 3f54613d86829..1dae7b1006de8 100644 --- a/src/bun.js/bindings/CachedScript.h +++ b/src/bun.js/bindings/CachedScript.h @@ -1,9 +1,8 @@ #pragma once -#include "root.h" - namespace WebCore { class CachedScript { }; -} \ No newline at end of file + +} diff --git a/src/bun.js/bindings/JSSocketAddress.cpp b/src/bun.js/bindings/JSSocketAddress.cpp index 1815073397954..8f478d4314374 100644 --- a/src/bun.js/bindings/JSSocketAddress.cpp +++ b/src/bun.js/bindings/JSSocketAddress.cpp @@ -1,5 +1,8 @@ -#include "JSSocketAddress.h" +#include "root.h" + #include "ZigGlobalObject.h" +#include "JSSocketAddress.h" + #include "JavaScriptCore/JSObjectInlines.h" #include "JavaScriptCore/ObjectConstructor.h" #include "JavaScriptCore/JSCast.h" @@ -9,6 +12,21 @@ using namespace JSC; namespace Bun { namespace JSSocketAddress { +JSObject* create(Zig::GlobalObject* globalObject, JSString* value, int32_t port, bool isIPv6) +{ + static const NeverDestroyed IPv4 = MAKE_STATIC_STRING_IMPL("IPv4"); + static const NeverDestroyed IPv6 = MAKE_STATIC_STRING_IMPL("IPv6"); + + VM& vm = globalObject->vm(); + + JSObject* thisObject = constructEmptyObject(vm, globalObject->JSSocketAddressStructure()); + thisObject->putDirectOffset(vm, 0, value); + thisObject->putDirectOffset(vm, 1, isIPv6 ? jsString(vm, IPv6) : jsString(vm, IPv4)); + thisObject->putDirectOffset(vm, 2, jsNumber(port)); + + return thisObject; +} + // Using a structure with inlined offsets should be more lightweight than a class. Structure* createStructure(VM& vm, JSGlobalObject* globalObject) { @@ -45,19 +63,7 @@ Structure* createStructure(VM& vm, JSGlobalObject* globalObject) } // namespace JSSocketAddress } // namespace Bun -extern "C" JSObject* JSSocketAddress__create(JSGlobalObject* globalObject, JSString* value, int32_t port, bool isIPv6) +extern "C" JSObject* JSSocketAddress__create(Zig::GlobalObject* globalObject, JSString* value, int32_t port, bool isIPv6) { - static const NeverDestroyed IPv4 = MAKE_STATIC_STRING_IMPL("IPv4"); - static const NeverDestroyed IPv6 = MAKE_STATIC_STRING_IMPL("IPv6"); - - VM& vm = globalObject->vm(); - - auto* global = jsCast(globalObject); - - JSObject* thisObject = constructEmptyObject(vm, global->JSSocketAddressStructure()); - thisObject->putDirectOffset(vm, 0, value); - thisObject->putDirectOffset(vm, 1, isIPv6 ? jsString(vm, IPv6) : jsString(vm, IPv4)); - thisObject->putDirectOffset(vm, 2, jsNumber(port)); - - return thisObject; + return Bun::JSSocketAddress::create(globalObject, value, port, isIPv6); } diff --git a/src/bun.js/bindings/JSSocketAddress.h b/src/bun.js/bindings/JSSocketAddress.h index 77bdca5d4fc94..8e6eed2eeea71 100644 --- a/src/bun.js/bindings/JSSocketAddress.h +++ b/src/bun.js/bindings/JSSocketAddress.h @@ -9,8 +9,9 @@ namespace Bun { namespace JSSocketAddress { Structure* createStructure(VM& vm, JSGlobalObject* globalObject); +JSObject* create(Zig::GlobalObject* globalObject, JSString* value, int port, bool isIPv6); } // namespace JSSocketAddress } // namespace Bun -extern "C" JSObject* JSSocketAddress__create(JSGlobalObject* globalObject, JSString* value, int port, bool isIPv6); +extern "C" JSObject* JSSocketAddress__create(Zig::GlobalObject* globalObject, JSString* value, int port, bool isIPv6); diff --git a/src/bun.js/bindings/NodeHTTP.cpp b/src/bun.js/bindings/NodeHTTP.cpp index ea44662172fdc..d206005b57325 100644 --- a/src/bun.js/bindings/NodeHTTP.cpp +++ b/src/bun.js/bindings/NodeHTTP.cpp @@ -5,36 +5,64 @@ #include "helpers.h" #include "BunClientData.h" -#include "JavaScriptCore/AggregateError.h" -#include "JavaScriptCore/InternalFieldTuple.h" -#include "JavaScriptCore/ObjectConstructor.h" -#include "JavaScriptCore/ObjectConstructor.h" -#include "JavaScriptCore/JSFunction.h" +#include +#include +#include +#include +#include #include "wtf/URL.h" #include "JSFetchHeaders.h" #include "JSDOMExceptionHandling.h" #include #include "ZigGeneratedClasses.h" +#include "ScriptExecutionContext.h" +#include "AsyncContextFrame.h" +#include "ZigGeneratedClasses.h" +#include +#include +#include "JSSocketAddress.h" + +extern "C" uint64_t uws_res_get_remote_address_info(void* res, const char** dest, int* port, bool* is_ipv6); namespace Bun { using namespace JSC; using namespace WebCore; +JSC_DEFINE_CUSTOM_SETTER(noOpSetter, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, PropertyName propertyName)) +{ + return false; +} + +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnClose); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterClosed); +JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnClose); +JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketClose); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterResponse); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterRemoteAddress); + +BUN_DECLARE_HOST_FUNCTION(Bun__drainMicrotasksFromJS); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex); +JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterDuplex); + // Create a static hash table of values containing an onclose DOMAttributeGetterSetter and a close function -static const struct HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] = { - { "onclose"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterOnClose, setterOnClose } }, - { "close"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, functionNodeHTTPServerSocket_close, 0 } }, +static const HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] = { + { "onclose"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnClose, jsNodeHttpServerSocketSetterOnClose } }, + { "closed"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterClosed, noOpSetter } }, + { "response"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterResponse, noOpSetter } }, + { "duplex"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterDuplex, jsNodeHttpServerSocketSetterDuplex } }, + { "remoteAddress"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterRemoteAddress, noOpSetter } }, + { "close"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketClose, 0 } }, }; class JSNodeHTTPServerSocketPrototype final : public JSC::JSNonFinalObject { public: using Base = JSC::JSNonFinalObject; - static JSNodeHTTPServerSocketPrototype* create(VM& vm, JSGlobalObject* globalObject, Structure* structure) + static JSNodeHTTPServerSocketPrototype* create(VM& vm, Structure* structure) { JSNodeHTTPServerSocketPrototype* prototype = new (NotNull, allocateCell(vm)) JSNodeHTTPServerSocketPrototype(vm, structure); - prototype->finishCreation(vm, globalObject); + prototype->finishCreation(vm); return prototype; } @@ -56,26 +84,29 @@ class JSNodeHTTPServerSocketPrototype final : public JSC::JSNonFinalObject { { } - void finishCreation(VM& vm, JSGlobalObject* globalObject) + void finishCreation(VM& vm) { Base::finishCreation(vm); ASSERT(inherits(info())); + reifyStaticProperties(vm, info(), JSNodeHTTPServerSocketPrototypeTableValues, *this); + this->structure()->setMayBePrototype(true); } }; class JSNodeHTTPServerSocket : public JSC::JSDestructibleObject { - using Base = JSC::JSDestructibleObject; - public: - static JSNodeHTTPServerSocket* create(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl) + using Base = JSC::JSDestructibleObject; + static JSNodeHTTPServerSocket* create(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) { - return new (JSC::allocateCell(vm)) JSNodeHTTPServerSocket(vm, structure, socket, is_ssl); + auto* object = new (JSC::allocateCell(vm)) JSNodeHTTPServerSocket(vm, structure, socket, is_ssl, response); + object->finishCreation(vm); + return object; } - static JSNodeHTTPServerSocket* create(JSC::VM& vm, Zig::GlobalObject* globalObject, us_socket_t* socket, bool is_ssl) + static JSNodeHTTPServerSocket* create(JSC::VM& vm, Zig::GlobalObject* globalObject, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) { auto* structure = globalObject->m_JSNodeHTTPServerSocketStructure.getInitializedOnMainThread(globalObject); - return create(vm, structure, socket, is_ssl); + return create(vm, structure, socket, is_ssl, response); } static void destroy(JSC::JSCell* cell) @@ -92,6 +123,19 @@ class JSNodeHTTPServerSocket : public JSC::JSDestructibleObject { } } + void close() + { + auto* socket = this->socket; + if (socket) { + us_socket_close(is_ssl, socket, 0, nullptr); + } + } + + bool isClosed() const + { + return !socket || us_socket_is_closed(is_ssl, socket); + } + ~JSNodeHTTPServerSocket() { if (socket) { @@ -103,17 +147,22 @@ class JSNodeHTTPServerSocket : public JSC::JSDestructibleObject { } } - JSNodeHTTPServerSocket(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl) + JSNodeHTTPServerSocket(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) : JSC::JSDestructibleObject(vm, structure) , socket(socket) , is_ssl(is_ssl) { + currentResponseObject.setEarlyValue(vm, this, response); } mutable WriteBarrier functionToCallOnClose; mutable WriteBarrier currentResponseObject; + mutable WriteBarrier m_remoteAddress; + mutable WriteBarrier m_duplex; + unsigned is_ssl : 1; us_socket_t* socket; + JSC::Strong strongThis = {}; DECLARE_INFO; DECLARE_VISIT_CHILDREN; @@ -134,28 +183,236 @@ class JSNodeHTTPServerSocket : public JSC::JSDestructibleObject { void onClose() { this->socket = nullptr; - } + this->m_duplex.clear(); + this->currentResponseObject.clear(); - static JSC_HOST_CALL_ATTRIBUTES JSC::EncodedJSValue onClose(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) - { - auto* thisObject = jsDynamicCast(callFrame->thisValue()); - if (!thisObject) { - return JSValue::encode(JSC::jsUndefined()); + // This function can be called during GC! + Zig::GlobalObject* globalObject = static_cast(this->globalObject()); + if (!functionToCallOnClose) { + this->strongThis.clear(); + + return; } - thisObject->onClose(); - return JSValue::encode(JSC::jsUndefined()); + WebCore::ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext(); + + if (scriptExecutionContext) { + JSC::gcProtect(this); + scriptExecutionContext->postTask([self = this](ScriptExecutionContext& context) { + WTF::NakedPtr exception; + auto* globalObject = defaultGlobalObject(context.globalObject()); + auto* thisObject = self; + auto* callbackObject = thisObject->functionToCallOnClose.get(); + if (!callbackObject) { + JSC::gcUnprotect(self); + return; + } + auto callData = JSC::getCallData(callbackObject); + MarkedArgumentBuffer args; + EnsureStillAliveScope ensureStillAlive(self); + JSC::gcUnprotect(self); + + if (globalObject->scriptExecutionStatus(globalObject, thisObject) == ScriptExecutionStatus::Running) { + profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, thisObject, args, exception); + + if (auto* ptr = exception.get()) { + exception.clear(); + globalObject->reportUncaughtExceptionAtEventLoop(globalObject, ptr); + } + } + }); + } + + this->strongThis.clear(); } static Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) { - JSC::JSObject* prototype = JSC::constructEmptyObject(vm, globalObject->nullPrototypeObjectStructure()); - prototype->structure()->setMayBePrototype(true); + auto* structure = JSC::Structure::create(vm, globalObject, globalObject->objectPrototype(), JSC::TypeInfo(JSC::ObjectType, StructureFlags), JSNodeHTTPServerSocketPrototype::info()); + auto* prototype = JSNodeHTTPServerSocketPrototype::create(vm, structure); + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } - return JSC::Structure::create(vm, globalObject, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + void finishCreation(JSC::VM& vm) + { + Base::finishCreation(vm); } }; +JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketClose, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + return JSValue::encode(JSC::jsUndefined()); + } + if (thisObject->isClosed()) { + return JSValue::encode(JSC::jsUndefined()); + } + thisObject->close(); + return JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + if (thisObject->m_duplex) { + return JSValue::encode(thisObject->m_duplex.get()); + } + return JSValue::encode(JSC::jsNull()); +} + +JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) +{ + auto& vm = globalObject->vm(); + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + JSValue value = JSC::JSValue::decode(encodedValue); + if (auto* object = value.getObject()) { + thisObject->m_duplex.set(vm, thisObject, object); + + } else { + thisObject->m_duplex.clear(); + } + + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterRemoteAddress, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto& vm = globalObject->vm(); + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + if (thisObject->m_remoteAddress) { + return JSValue::encode(thisObject->m_remoteAddress.get()); + } + + us_socket_t* socket = thisObject->socket; + if (!socket) { + return JSValue::encode(JSC::jsNull()); + } + + const char* address = nullptr; + int port = 0; + bool is_ipv6 = false; + + uws_res_get_remote_address_info(socket, &address, &port, &is_ipv6); + + if (address == nullptr) { + return JSValue::encode(JSC::jsNull()); + } + + auto addressString = WTF::String::fromUTF8(address); + if (addressString.isEmpty()) { + return JSValue::encode(JSC::jsNull()); + } + + auto* object = JSSocketAddress::create(defaultGlobalObject(globalObject), jsString(vm, addressString), port, is_ipv6); + thisObject->m_remoteAddress.set(vm, thisObject, object); + return JSValue::encode(object); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnClose, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + + if (thisObject->functionToCallOnClose) { + return JSValue::encode(thisObject->functionToCallOnClose.get()); + } + + return JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnClose, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + JSValue value = JSC::JSValue::decode(encodedValue); + + if (value.isUndefined() || value.isNull()) { + thisObject->functionToCallOnClose.clear(); + return true; + } + + if (!value.isCallable()) { + return false; + } + + thisObject->functionToCallOnClose.set(vm, thisObject, value.getObject()); + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterClosed, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + return JSValue::encode(JSC::jsBoolean(thisObject->isClosed())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterResponse, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + if (!thisObject->currentResponseObject) { + return JSValue::encode(JSC::jsNull()); + } + + return JSValue::encode(thisObject->currentResponseObject.get()); +} + +template +void JSNodeHTTPServerSocket::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + JSNodeHTTPServerSocket* fn = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(fn, info()); + Base::visitChildren(fn, visitor); + + visitor.append(fn->currentResponseObject); + visitor.append(fn->functionToCallOnClose); + visitor.append(fn->m_remoteAddress); + visitor.append(fn->m_duplex); +} + +DEFINE_VISIT_CHILDREN(JSNodeHTTPServerSocket); + +template +static JSNodeHTTPServerSocket* getNodeHTTPServerSocket(us_socket_t* socket) +{ + auto* httpResponseData = (uWS::HttpResponseData*)us_socket_ext(SSL, socket); + return reinterpret_cast(httpResponseData->socketData); +} + +template +static WebCore::JSNodeHTTPResponse* getNodeHTTPResponse(us_socket_t* socket) +{ + auto* serverSocket = getNodeHTTPServerSocket(socket); + if (!serverSocket) { + return nullptr; + } + return serverSocket->currentResponseObject.get(); +} + +const JSC::ClassInfo JSNodeHTTPServerSocket::s_info = { "NodeHTTPServerSocket"_s, &Base::s_info, nullptr, nullptr, + CREATE_METHOD_TABLE(JSNodeHTTPServerSocket) }; + +const JSC::ClassInfo JSNodeHTTPServerSocketPrototype::s_info = { "NodeHTTPServerSocket"_s, &Base::s_info, nullptr, nullptr, + CREATE_METHOD_TABLE(JSNodeHTTPServerSocketPrototype) }; + +template +static void* getNodeHTTPResponsePtr(us_socket_t* socket) +{ + WebCore::JSNodeHTTPResponse* responseObject = getNodeHTTPResponse(socket); + if (!responseObject) { + return nullptr; + } + return responseObject->wrapped(); +} + +extern "C" EncodedJSValue Bun__getNodeHTTPResponseThisValue(int is_ssl, us_socket_t* socket) +{ + if (is_ssl) { + return JSValue::encode(getNodeHTTPResponse(socket)); + } + return JSValue::encode(getNodeHTTPResponse(socket)); +} + BUN_DECLARE_HOST_FUNCTION(jsFunctionRequestOrResponseHasBodyValue); BUN_DECLARE_HOST_FUNCTION(jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer); extern "C" uWS::HttpRequest* Request__getUWSRequest(void*); @@ -553,12 +810,30 @@ static EncodedJSValue assignHeadersFromUWebSockets(uWS::HttpRequest* request, JS return JSValue::encode(tuple); } +template +static void assignOnCloseFunction(uWS::TemplatedApp* app) +{ + app->setOnClose([](void* socketData, int is_ssl, struct us_socket_t* rawSocket) -> void { + auto* socket = reinterpret_cast(socketData); + ASSERT(rawSocket == socket->socket || socket->socket == nullptr); + socket->onClose(); + }); +} + +extern "C" void NodeHTTP_assignOnCloseFunction(int is_ssl, void* uws_app) +{ + if (is_ssl) { + assignOnCloseFunction(reinterpret_cast*>(uws_app)); + } else { + assignOnCloseFunction(reinterpret_cast*>(uws_app)); + } +} extern "C" EncodedJSValue NodeHTTPResponse__createForJS(size_t any_server, JSC::JSGlobalObject* globalObject, int* hasBody, uWS::HttpRequest* request, int isSSL, void* response_ptr, void** nodeHttpResponsePtr); template static EncodedJSValue NodeHTTPServer__onRequest( size_t any_server, - JSC::JSGlobalObject* globalObject, + Zig::GlobalObject* globalObject, JSValue thisValue, JSValue callback, uWS::HttpRequest* request, @@ -581,12 +856,39 @@ static EncodedJSValue NodeHTTPServer__onRequest( } int hasBody = 0; - EncodedJSValue nodehttpobjectValue = NodeHTTPResponse__createForJS(any_server, globalObject, &hasBody, request, isSSL, response, nodeHttpResponsePtr); + WebCore::JSNodeHTTPResponse* nodeHTTPResponseObject = jsCast(JSValue::decode(NodeHTTPResponse__createForJS(any_server, globalObject, &hasBody, request, isSSL, response, nodeHttpResponsePtr))); JSC::CallData callData = getCallData(callbackObject); - args.append(JSValue::decode(nodehttpobjectValue)); + args.append(nodeHTTPResponseObject); args.append(jsBoolean(hasBody)); + auto* currentSocketDataPtr = reinterpret_cast(response->getHttpResponseData()->socketData); + + if (currentSocketDataPtr) { + auto* thisSocket = jsCast(currentSocketDataPtr); + thisSocket->currentResponseObject.set(vm, thisSocket, nodeHTTPResponseObject); + args.append(thisSocket); + args.append(jsBoolean(true)); + if (thisSocket->m_duplex) { + args.append(thisSocket->m_duplex.get()); + } else { + args.append(jsUndefined()); + } + } else { + JSNodeHTTPServerSocket* socket = JSNodeHTTPServerSocket::create( + vm, + globalObject->m_JSNodeHTTPServerSocketStructure.getInitializedOnMainThread(globalObject), + (us_socket_t*)response, + isSSL, nodeHTTPResponseObject); + + socket->strongThis.set(vm, socket); + response->getHttpResponseData()->socketData = socket; + + args.append(socket); + args.append(jsBoolean(false)); + args.append(jsUndefined()); + } + WTF::NakedPtr exception; JSValue returnValue = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, jsUndefined(), args, exception); if (exception) { @@ -697,6 +999,9 @@ static void NodeHTTPServer__writeHead( auto scope = DECLARE_THROW_SCOPE(vm); JSObject* headersObject = headersObjectValue.getObject(); + if (response->getLoopData()->canCork() && response->getBufferedAmount() == 0) { + response->getLoopData()->setCorkedSocket(response, isSSL); + } response->writeStatus(std::string_view(statusMessage, statusMessageLength)); if (headersObject) { @@ -774,7 +1079,7 @@ extern "C" void NodeHTTPServer__writeHead_https( extern "C" EncodedJSValue NodeHTTPServer__onRequest_http( size_t any_server, - JSC::JSGlobalObject* globalObject, + Zig::GlobalObject* globalObject, EncodedJSValue thisValue, EncodedJSValue callback, uWS::HttpRequest* request, @@ -786,7 +1091,7 @@ extern "C" EncodedJSValue NodeHTTPServer__onRequest_http( extern "C" EncodedJSValue NodeHTTPServer__onRequest_https( size_t any_server, - JSC::JSGlobalObject* globalObject, + Zig::GlobalObject* globalObject, EncodedJSValue thisValue, EncodedJSValue callback, uWS::HttpRequest* request, @@ -1053,6 +1358,9 @@ JSValue createNodeHTTPInternalBinding(Zig::GlobalObject* globalObject) obj->putDirectNativeFunction( vm, globalObject, JSC::PropertyName(JSC::Identifier::fromString(vm, "getCompleteWebRequestOrResponseBodyValueAsArrayBuffer"_s)), 1, jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer, ImplementationVisibility::Public, Intrinsic::NoIntrinsic, 0); + obj->putDirectNativeFunction( + vm, globalObject, JSC::PropertyName(JSC::Identifier::fromString(vm, "drainMicrotasks"_s)), + 0, Bun__drainMicrotasksFromJS, ImplementationVisibility::Public, Intrinsic::NoIntrinsic, 0); return obj; } @@ -1065,4 +1373,9 @@ extern "C" void WebCore__FetchHeaders__toUWSResponse(WebCore::FetchHeaders* arg0 } } +JSC::Structure* createNodeHTTPServerSocketStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + return JSNodeHTTPServerSocket::createStructure(vm, globalObject); +} + } // namespace Bun diff --git a/src/bun.js/bindings/NodeHTTP.h b/src/bun.js/bindings/NodeHTTP.h index b682d826b2e11..8035cc216c846 100644 --- a/src/bun.js/bindings/NodeHTTP.h +++ b/src/bun.js/bindings/NodeHTTP.h @@ -6,7 +6,7 @@ JSC_DECLARE_HOST_FUNCTION(jsHTTPAssignHeaders); JSC_DECLARE_HOST_FUNCTION(jsHTTPGetHeader); JSC_DECLARE_HOST_FUNCTION(jsHTTPSetHeader); -JSC::Structure* createNodeHTTPServerSocketStructure(JSC::VM& vm); +JSC::Structure* createNodeHTTPServerSocketStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject); JSC::JSValue createNodeHTTPInternalBinding(Zig::GlobalObject*); } diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 2039774239d22..7ee62e55ea9f0 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -2689,7 +2689,7 @@ void GlobalObject::finishCreation(VM& vm) Bun::addNodeModuleConstructorProperties(vm, this); m_JSNodeHTTPServerSocketStructure.initLater( [](const Initializer& init) { - init.set(Bun::createNodeHTTPServerSocketStructure(init.vm)); + init.set(Bun::createNodeHTTPServerSocketStructure(init.vm, init.owner)); }); m_JSDOMFileConstructor.initLater( [](const Initializer& init) { diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index dad8da4bee8ca..61af5bf48c803 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -2565,6 +2565,12 @@ pub const VirtualMachine = struct { pub const main_file_name: string = "bun:main"; + pub export fn Bun__drainMicrotasksFromJS(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + _ = callframe; // autofix + globalObject.bunVM().drainMicrotasks(); + return .undefined; + } + pub fn drainMicrotasks(this: *VirtualMachine) void { this.eventLoop().drainMicrotasks(); } diff --git a/src/deps/uws.zig b/src/deps/uws.zig index e1e2d34c9a7d7..463d7f7b79439 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -3040,6 +3040,12 @@ pub const AnyResponse = union(enum) { SSL: *NewApp(true).Response, TCP: *NewApp(false).Response, + pub fn socket(this: AnyResponse) *uws_res { + return switch (this) { + .SSL => |resp| resp.downcast(), + .TCP => |resp| resp.downcast(), + }; + } pub fn getRemoteSocketInfo(this: AnyResponse) ?SocketAddress { return switch (this) { .SSL => |resp| resp.getRemoteSocketInfo(), diff --git a/src/js/node/http.ts b/src/js/node/http.ts index 45f7f81cdea84..0674b0c9f4509 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -55,6 +55,8 @@ const statusMessageSymbol = Symbol("statusMessage"); const kInternalSocketData = Symbol.for("::bunternal::"); const serverSymbol = Symbol.for("::bunternal::"); +const kRequest = Symbol("request"); + const kEmptyObject = Object.freeze(Object.create(null)); const EventEmitter: typeof import("node:events").EventEmitter = require("node:events"); const { isTypedArray } = require("node:util/types"); @@ -79,6 +81,7 @@ const { headersTuple, webRequestOrResponseHasBodyValue, getCompleteWebRequestOrResponseBodyValueAsArrayBuffer, + drainMicrotasks, } = $cpp("NodeHTTP.cpp", "createNodeHTTPInternalBinding") as { getHeader: (headers: Headers, name: string) => string | undefined; setHeader: (headers: Headers, name: string, value: string) => void; @@ -298,6 +301,133 @@ var FakeSocket = class Socket extends Duplex { _write(chunk, encoding, callback) {} }; +const NodeHTTPServerSocket = class Socket extends Duplex { + bytesRead = 0; + bytesWritten = 0; + connecting = false; + timeout = 0; + [kHandle]; + server: Server; + _httpMessage; + + constructor(server: Server, handle, encrypted) { + super(); + this.server = server; + this[kHandle] = handle; + handle.onclose = this.#onClose.bind(this); + handle.duplex = this; + this.encrypted = encrypted; + } + + #onClose(callback) { + this[kHandle] = null; + if ($isCallable(callback)) callback(); + } + + address() { + return this[kHandle]?.remoteAddress || null; + } + + get bufferSize() { + return this.writableLength; + } + + connect(port, host, connectListener) { + return this; + } + + _destroy(err, callback) { + const handle = this[kHandle]; + if (!handle) return; // sometimes 'this' is Socket not FakeSocket + this[kHandle] = undefined; + handle.onclose = this.#onClose.bind(this, callback); + handle.close(); + } + + _final(callback) { + const handle = this[kHandle]; + if (!handle) return; // sometimes 'this' is Socket not FakeSocket + handle.onclose = this.#onClose.bind(this, callback); + handle.close(); + } + + get localAddress() { + return "127.0.0.1"; + } + + get localFamily() { + return "IPv4"; + } + + get localPort() { + return 80; + } + + get pending() { + return this.connecting; + } + + _read(size) {} + + get readyState() { + if (this.connecting) return "opening"; + if (this.readable) { + return this.writable ? "open" : "readOnly"; + } else { + return this.writable ? "writeOnly" : "closed"; + } + } + + ref() { + return this; + } + + get remoteAddress() { + return this.address()?.address; + } + + set remoteAddress(val) { + // initialize the object so that other properties wouldn't be lost + this.address().address = val; + } + + get remotePort() { + return this.address()?.port; + } + + set remotePort(val) { + // initialize the object so that other properties wouldn't be lost + this.address().port = val; + } + + get remoteFamily() { + return this.address()?.family; + } + + set remoteFamily(val) { + // initialize the object so that other properties wouldn't be lost + this.address().family = val; + } + + resetAndDestroy() {} + + setKeepAlive(enable = false, initialDelay = 0) {} + + setNoDelay(noDelay = true) { + return this; + } + + setTimeout(timeout, callback) { + return this; + } + + unref() { + return this; + } + + _write(chunk, encoding, callback) {} +} as unknown as typeof import("node:net").Socket; + function createServer(options, callback) { return new Server(options, callback); } @@ -393,7 +523,8 @@ function emitListeningNextTick(self, hostname, port) { } } -function Server(options, callback) { +type Server = InstanceType; +const Server = function Server(options, callback) { if (!(this instanceof Server)) return new Server(options, callback); EventEmitter.$call(this); @@ -470,7 +601,7 @@ function Server(options, callback) { if (callback) this.on("request", callback); return this; -} +} as unknown as typeof import("node:http").Server; function onRequestEvent(event) { const [server, http_res, req] = this.socket[kInternalSocketData]; @@ -490,6 +621,23 @@ function onRequestEvent(event) { } } +function onServerRequestEvent(this: NodeHTTPServerSocket, event: NodeHTTPResponseAbortEvent) { + const server: Server = this.server; + const socket: NodeHTTPServerSocket = this; + + switch (event) { + case NodeHTTPResponseAbortEvent.abort: { + socket.destroy(); + break; + } + case NodeHTTPResponseAbortEvent.timeout: { + socket.emit("timeout"); + server.emit("timeout", socket); + break; + } + } +} + Server.prototype = { ref() { this._unref = false; @@ -690,11 +838,17 @@ Server.prototype = { headersArray: string[], handle, hasBody: boolean, + socketHandle, + isSocketNew, + socket, ) { const prevIsNextIncomingMessageHTTPS = isNextIncomingMessageHTTPS; isNextIncomingMessageHTTPS = isHTTPS; + if (!socket) { + socket = new NodeHTTPServerSocket(server, socketHandle, !!tls); + } - const http_req = new RequestClass(kHandle, url, method, headersObject, headersArray, handle, hasBody); + const http_req = new RequestClass(kHandle, url, method, headersObject, headersArray, handle, hasBody, socket); const http_res = new ResponseClass(http_req, { [kHandle]: handle, }); @@ -712,32 +866,42 @@ Server.prototype = { let resolveFunction; let didFinish = false; - let closeCallback = () => { - didFinish = true; - handle && (handle.onabort = undefined); - if (resolveFunction) resolveFunction(http_res); - handle = undefined; - }; http_req.once("error", errorCallback); http_res.once("error", errorCallback); - handle.onabort = onRequestEvent.bind(http_req); - const socket = http_req.socket; - socket[kInternalSocketData] = [server, http_res, handle]; - server.emit("connection", socket); + // handle.onabort = onServerRequestEvent.bind(socketHandle); + http_res.once("close", () => { + didFinish = true; + resolveFunction && resolveFunction(); + }); + + if (isSocketNew) { + server.emit("connection", socket); + } + + socket._httpMessage = http_res; + http_res.socket = socket; + socket[kRequest] = http_req; + server.emit("request", http_req, http_res); + socket.cork(drainMicrotasks); + if (capturedError) { handle = undefined; + if (socket._httpMessage === http_res) { + socket._httpMessage = null; + } throw capturedError; } if (handle.finished || didFinish) { http_res.off("error", errorCallback); - http_res.off("close", closeCallback); handle = undefined; - closeCallback = () => {}; + if (socket._httpMessage === http_res) { + socket._httpMessage = null; + } return; } @@ -921,15 +1085,6 @@ function IncomingMessage(req, defaultIncomingOpts) { this.complete = false; this._closed = false; - if (isNextIncomingMessageHTTPS) { - // Creating a new Duplex is expensive. - // We can skip it if the request is not HTTPS. - const socket = new FakeSocket(); - this[fakeSocketSymbol] = socket; - socket.encrypted = true; - isNextIncomingMessageHTTPS = false; - } - // (url, method, headers, rawHeaders, handle, hasBody) if (req === kHandle) { this[typeSymbol] = NodeHTTPIncomingRequestType.NodeHTTPResponse; @@ -939,8 +1094,18 @@ function IncomingMessage(req, defaultIncomingOpts) { this.rawHeaders = arguments[4]; this[kHandle] = arguments[5]; this[noBodySymbol] = !arguments[6]; + this[fakeSocketSymbol] = arguments[7]; Readable.$call(this); } else { + if (isNextIncomingMessageHTTPS) { + // Creating a new Duplex is expensive. + // We can skip it if the request is not HTTPS. + const socket = new FakeSocket(); + this[fakeSocketSymbol] = socket; + socket.encrypted = true; + isNextIncomingMessageHTTPS = false; + } + this[noBodySymbol] = false; Readable.$call(this); var { [typeSymbol]: type = NodeHTTPIncomingRequestType.FetchRequest, [reqSymbol]: nodeReq } = @@ -1509,6 +1674,7 @@ function callWriteHeadIfObservable(self, headerState) { !(self.writeHead === OriginalWriteHeadFn && self._implicitHeader === OriginalImplicitHeadFn) ) { self.writeHead(self.statusCode, self.statusMessage, self[headersSymbol]); + console.log("called rwriteHead"); } } @@ -1576,6 +1742,7 @@ const ServerResponsePrototype = { handle.end(chunk, encoding); } } + this._header = " "; const req = this.req; const reqClosed = req?._closed; if (reqClosed === false && req) { @@ -1752,7 +1919,7 @@ const ServerResponsePrototype = { throw ERR_HTTP_SOCKET_ASSIGNED(); } socket._httpMessage = this; - socket.on("close", () => onServerResponseClose.$call(socket)); + socket.once("close", () => onServerResponseClose.$call(socket)); this.socket = socket; this.emit("socket", socket); }, @@ -1796,6 +1963,7 @@ const ServerResponsePrototype = { const handle = this[kHandle]; if (handle && !this.headersSent) { + this[headerStateSymbol] = NodeHTTPHeaderState.sent; handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]); } }, @@ -2893,7 +3061,7 @@ const setMaxHTTPHeaderSize = $newZigFunction("node_http_binding.zig", "setMaxHTT const getMaxHTTPHeaderSize = $newZigFunction("node_http_binding.zig", "getMaxHTTPHeaderSize", 0); var globalAgent = new Agent(); -export default { +const http_exports = { Agent, Server, METHODS, @@ -2918,3 +3086,5 @@ export default { ClientRequest, OutgoingMessage, }; + +export default http_exports; From b7e06a979a3948e67005f439851cd2d080bcaea7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 10 Oct 2024 16:48:01 -0700 Subject: [PATCH 16/36] Add some more node tests --- .../node/test/parallel/http-hex-write.test.js | 60 +++++++++++ .../parallel/http-keepalive-request.test.js | 99 +++++++++++++++++++ ...-pipeline-requests-connection-leak.test.js | 50 ++++++++++ .../parallel/http-readable-data-event.test.js | 75 ++++++++++++++ ...-response-remove-header-after-sent.test.js | 30 ++++++ 5 files changed, 314 insertions(+) create mode 100644 test/js/node/test/parallel/http-hex-write.test.js create mode 100644 test/js/node/test/parallel/http-keepalive-request.test.js create mode 100644 test/js/node/test/parallel/http-pipeline-requests-connection-leak.test.js create mode 100644 test/js/node/test/parallel/http-readable-data-event.test.js create mode 100644 test/js/node/test/parallel/http-response-remove-header-after-sent.test.js diff --git a/test/js/node/test/parallel/http-hex-write.test.js b/test/js/node/test/parallel/http-hex-write.test.js new file mode 100644 index 0000000000000..1606bcac188a7 --- /dev/null +++ b/test/js/node/test/parallel/http-hex-write.test.js @@ -0,0 +1,60 @@ +//#FILE: test-http-hex-write.js +//#SHA1: 77a5322a8fe08e8505f39d42614167d223c9fbb0 +//----------------- +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +"use strict"; +const http = require("http"); + +const expectedResponse = "hex\nutf8\n"; + +test("HTTP server writes hex and utf8", async () => { + const server = http.createServer((req, res) => { + res.setHeader("content-length", expectedResponse.length); + res.write("6865780a", "hex"); + res.write("utf8\n"); + res.end(); + server.close(); + }); + + await new Promise(resolve => { + server.listen(0, () => { + const port = server.address().port; + http + .request({ port }) + .on("response", res => { + let data = ""; + res.setEncoding("ascii"); + res.on("data", chunk => { + data += chunk; + }); + res.on("end", () => { + expect(data).toBe(expectedResponse); + resolve(); + }); + }) + .end(); + }); + }); +}); + +//<#END_FILE: test-http-hex-write.js diff --git a/test/js/node/test/parallel/http-keepalive-request.test.js b/test/js/node/test/parallel/http-keepalive-request.test.js new file mode 100644 index 0000000000000..956c421a4a50a --- /dev/null +++ b/test/js/node/test/parallel/http-keepalive-request.test.js @@ -0,0 +1,99 @@ +//#FILE: test-http-keepalive-request.js +//#SHA1: 31cc9b875e1ead9a0b98fb07b672b536f6d06fba +//----------------- +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +"use strict"; + +const http = require("http"); + +let serverSocket = null; +let clientSocket = null; +const expectRequests = 10; +let actualRequests = 0; + +const server = http.createServer((req, res) => { + // They should all come in on the same server socket. + if (serverSocket) { + expect(req.socket).toBe(serverSocket); + } else { + serverSocket = req.socket; + } + + res.end(req.url); +}); + +const agent = new http.Agent({ keepAlive: true }); + +function makeRequest(n) { + return new Promise(resolve => { + if (n === 0) { + server.close(); + agent.destroy(); + resolve(); + return; + } + + const req = http.request({ + port: server.address().port, + path: `/${n}`, + agent: agent, + }); + + req.end(); + + req.on("socket", sock => { + if (clientSocket) { + expect(sock).toBe(clientSocket); + } else { + clientSocket = sock; + } + }); + + req.on("response", res => { + let data = ""; + res.setEncoding("utf8"); + res.on("data", c => { + data += c; + }); + res.on("end", () => { + expect(data).toBe(`/${n}`); + setTimeout(() => { + actualRequests++; + resolve(makeRequest(n - 1)); + }, 1); + }); + }); + }); +} + +test("HTTP keep-alive requests", async () => { + await new Promise(resolve => { + server.listen(0, () => { + resolve(makeRequest(expectRequests)); + }); + }); + + expect(actualRequests).toBe(expectRequests); +}); + +//<#END_FILE: test-http-keepalive-request.js diff --git a/test/js/node/test/parallel/http-pipeline-requests-connection-leak.test.js b/test/js/node/test/parallel/http-pipeline-requests-connection-leak.test.js new file mode 100644 index 0000000000000..23234f80bc181 --- /dev/null +++ b/test/js/node/test/parallel/http-pipeline-requests-connection-leak.test.js @@ -0,0 +1,50 @@ +//#FILE: test-http-pipeline-requests-connection-leak.js +//#SHA1: fc3e33a724cc7a499c7716fe8af6b78e7f72e943 +//----------------- +"use strict"; + +const http = require("http"); +const net = require("net"); + +const big = Buffer.alloc(16 * 1024, "A"); + +const COUNT = 1e4; + +test("HTTP pipeline requests do not cause connection leak", done => { + let client; + const server = http.createServer((req, res) => { + res.end(big, () => { + countdown.dec(); + }); + }); + + const countdown = new Countdown(COUNT, () => { + server.close(); + client.end(); + done(); + }); + + server.listen(0, () => { + const req = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".repeat(COUNT); + client = net.connect(server.address().port, () => { + client.write(req); + }); + client.resume(); + }); +}); + +class Countdown { + constructor(count, callback) { + this.count = count; + this.callback = callback; + } + + dec() { + this.count--; + if (this.count === 0) { + this.callback(); + } + } +} + +//<#END_FILE: test-http-pipeline-requests-connection-leak.js diff --git a/test/js/node/test/parallel/http-readable-data-event.test.js b/test/js/node/test/parallel/http-readable-data-event.test.js new file mode 100644 index 0000000000000..238c44878b8bd --- /dev/null +++ b/test/js/node/test/parallel/http-readable-data-event.test.js @@ -0,0 +1,75 @@ +//#FILE: test-http-readable-data-event.js +//#SHA1: a094638e155550b5bc5ecdf5b11c54e8124d1dc1 +//----------------- +"use strict"; + +const http = require("http"); +const helloWorld = "Hello World!"; +const helloAgainLater = "Hello again later!"; + +let next = null; + +test("HTTP readable data event", async () => { + const server = http.createServer((req, res) => { + res.writeHead(200, { + "Content-Length": `${helloWorld.length + helloAgainLater.length}`, + }); + + // We need to make sure the data is flushed + // before writing again + next = () => { + res.end(helloAgainLater); + next = () => {}; + }; + + res.write(helloWorld); + }); + + await new Promise(resolve => server.listen(0, resolve)); + + const opts = { + hostname: "localhost", + port: server.address().port, + path: "/", + }; + + const expectedData = [helloWorld, helloAgainLater]; + const expectedRead = [helloWorld, null, helloAgainLater, null, null]; + + await new Promise((resolve, reject) => { + const req = http.request(opts, res => { + res.on("error", reject); + + const readableSpy = jest.fn(); + res.on("readable", readableSpy); + + res.on("readable", () => { + let data; + + do { + data = res.read(); + expect(data).toBe(expectedRead.shift()); + next(); + } while (data !== null); + }); + + res.setEncoding("utf8"); + const dataSpy = jest.fn(); + res.on("data", dataSpy); + + res.on("data", data => { + expect(data).toBe(expectedData.shift()); + }); + + res.on("end", () => { + expect(readableSpy).toHaveBeenCalledTimes(3); + expect(dataSpy).toHaveBeenCalledTimes(2); + server.close(() => resolve()); + }); + }); + + req.end(); + }); +}); + +//<#END_FILE: test-http-readable-data-event.js diff --git a/test/js/node/test/parallel/http-response-remove-header-after-sent.test.js b/test/js/node/test/parallel/http-response-remove-header-after-sent.test.js new file mode 100644 index 0000000000000..c01808e44067b --- /dev/null +++ b/test/js/node/test/parallel/http-response-remove-header-after-sent.test.js @@ -0,0 +1,30 @@ +//#FILE: test-http-response-remove-header-after-sent.js +//#SHA1: df9a9a2f545c88b70d6d33252a1568339ea6f5b3 +//----------------- +"use strict"; + +const http = require("http"); + +test("remove header after response is sent", done => { + const server = http.createServer((req, res) => { + res.removeHeader("header1", 1); + res.write("abc"); + expect(() => res.removeHeader("header2", 2)).toThrow( + expect.objectContaining({ + code: "ERR_HTTP_HEADERS_SENT", + name: "Error", + message: expect.any(String), + }), + ); + res.end(); + }); + + server.listen(0, () => { + http.get({ port: server.address().port }, () => { + server.close(); + done(); + }); + }); +}); + +//<#END_FILE: test-http-response-remove-header-after-sent.js From dae59b687a47b8c559f04ae728a0a181f6776450 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 14 Oct 2024 02:19:52 -0700 Subject: [PATCH 17/36] Fix a couple tests --- test/js/node/http/node-http.test.ts | 92 +++++++++++++++++------------ 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/test/js/node/http/node-http.test.ts b/test/js/node/http/node-http.test.ts index 814d2780f8c38..b394036684e79 100644 --- a/test/js/node/http/node-http.test.ts +++ b/test/js/node/http/node-http.test.ts @@ -63,7 +63,7 @@ describe("node:http", () => { it("is not marked encrypted (#5867)", async () => { try { var server = createServer((req, res) => { - expect(req.connection.encrypted).toBe(undefined); + expect(req.connection.encrypted).toBe(false); res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Hello World"); }); @@ -771,62 +771,73 @@ describe("node:http", () => { }); }); - it("request via http proxy, issue#4295", done => { - const proxyServer = createServer(function (req, res) { - let option = url.parse(req.url); - option.host = req.headers.host; - option.headers = req.headers; + it("request via http proxy, issue#4295", async () => { + const http = require("http"); + const defer = Promise.withResolvers(); + let wentThroughProxy = false; - const proxyRequest = request(option, function (proxyResponse) { - res.writeHead(proxyResponse.statusCode, proxyResponse.headers); - proxyResponse.on("data", function (chunk) { - res.write(chunk, "binary"); - }); - proxyResponse.on("end", function () { - res.end(); + const proxyServer = http.createServer((req, res) => { + fetch("http://example.com/" + req.url, { + headers: { + "X-Bun": "true", + }, + }) + .then(response => { + response.body.pipeTo( + new WritableStream({ + write(chunk) { + res.write(chunk); + }, + close() { + res.end(); + wentThroughProxy = true; + }, + }), + ); + }) + .catch(error => { + defer.reject(error); }); - }); - req.on("data", function (chunk) { - proxyRequest.write(chunk, "binary"); - }); - req.on("end", function () { - proxyRequest.end(); - }); }); - proxyServer.listen({ port: 0 }, async (_err, hostname, port) => { + proxyServer.listen({ port: 0 }, () => { const options = { protocol: "http:", - hostname: hostname, - port: port, - path: "http://example.com", + hostname: "127.0.0.1", + port: proxyServer.address().port, + path: "http://example.com/something", headers: { Host: "example.com", - "accept-encoding": "identity", }, }; - const req = request(options, res => { + const req = http.request(options, res => { let data = ""; + res.on("data", chunk => { data += chunk; }); + + res.on("error", error => { + defer.reject(error); + }); + res.on("end", () => { - try { - expect(res.statusCode).toBe(200); - expect(data.length).toBeGreaterThan(0); - expect(data).toContain("This domain is for use in illustrative examples in documents"); - done(); - } catch (err) { - done(err); - } + defer.resolve(data); }); }); - req.on("error", err => { - done(err); + + req.on("error", error => { + defer.reject(error); }); + req.end(); }); + + const result = await defer.promise; + proxyServer.close(); + expect(result.toLowerCase()).toStartWith(`\n\n { @@ -2374,14 +2385,17 @@ it("should emit timeout event when using server.setTimeout", async () => { let callBackCalled = false; server.setTimeout(1000, () => { callBackCalled = true; + console.log("Called timeout"); }); - fetch(`http://localhost:${server.address().port}`) + + fetch(`http://localhost:${server.address().port}`, { verbose: true }) .then(res => res.text()) - .catch(() => {}); + .catch(err => { + console.log(err); + }); const [req, res] = await once(server, "request"); expect(req.complete).toBe(false); - await once(server, "timeout"); expect(callBackCalled).toBe(true); } finally { From da358332677864f7c7c3c7fe0c3c4570befc08c4 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 14 Oct 2024 06:10:32 -0700 Subject: [PATCH 18/36] getting closer --- src/bun.js/api/server.classes.ts | 8 + src/bun.js/api/server.zig | 32 +- src/bun.js/event_loop.zig | 118 ++--- src/js/node/http.ts | 195 ++++--- test/js/node/http/node-http.test.ts | 475 ++---------------- .../express-body-parser-test.test.ts | 12 + 6 files changed, 285 insertions(+), 555 deletions(-) diff --git a/src/bun.js/api/server.classes.ts b/src/bun.js/api/server.classes.ts index d284e718d950e..b68dd4012dedc 100644 --- a/src/bun.js/api/server.classes.ts +++ b/src/bun.js/api/server.classes.ts @@ -120,6 +120,14 @@ export default [ fn: "abort", length: 0, }, + pause: { + fn: "doPause", + length: 0, + }, + resume: { + fn: "doResume", + length: 0, + }, bufferedAmount: { getter: "getBufferedAmount", }, diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 6e21ff4a8f2d0..d5132139654f6 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -5862,6 +5862,7 @@ pub const NodeHTTPResponse = struct { this.clearJSValues(); this.clearOnDataCallback(); const server = this.server; + this.js_ref.unref(JSC.VirtualMachine.get()); this.deref(); server.onRequestComplete(); } @@ -6080,8 +6081,7 @@ pub const NodeHTTPResponse = struct { timeout = 2, }; - fn handleAbortOrTimeout(this: *NodeHTTPResponse, resp: uws.AnyResponse, comptime event: AbortEvent) void { - _ = resp; // autofix + fn handleAbortOrTimeout(this: *NodeHTTPResponse, comptime event: AbortEvent) void { if (this.finished) { return; } @@ -6116,13 +6116,37 @@ pub const NodeHTTPResponse = struct { } pub fn onAbort(this: *NodeHTTPResponse, response: uws.AnyResponse) void { + _ = response; // autofix log("onAbort", .{}); - this.handleAbortOrTimeout(response, .abort); + this.handleAbortOrTimeout(.abort); } pub fn onTimeout(this: *NodeHTTPResponse, response: uws.AnyResponse) void { + _ = response; // autofix log("onTimeout", .{}); - this.handleAbortOrTimeout(response, .timeout); + this.handleAbortOrTimeout(.timeout); + } + + pub fn doPause(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + _ = globalObject; // autofix + _ = callframe; // autofix + if (this.finished or this.aborted) { + return .undefined; + } + + this.response.pause(); + return .undefined; + } + + pub fn doResume(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + _ = globalObject; // autofix + _ = callframe; // autofix + if (this.finished or this.aborted) { + return .undefined; + } + + this.response.@"resume"(); + return .undefined; } fn onRequestComplete(this: *NodeHTTPResponse) void { diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 203b20efec229..f55647b01da3f 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -406,84 +406,84 @@ const ServerAllConnectionsClosedTask = @import("./api/server.zig").ServerAllConn // Task.get(ReadFileTask) -> ?ReadFileTask pub const Task = TaggedPointerUnion(.{ - FetchTasklet, + Access, + AnyTask, + AppendFile, AsyncGlobWalkTask, AsyncTransformTask, - ReadFileTask, + bun.bake.DevServer.HotReloadTask, + bun.shell.Interpreter.Builtin.Yes.YesTask, + Chmod, + Chown, + Close, + CopyFile, CopyFilePromiseTask, - WriteFileTask, - AnyTask, - ManagedTask, - ShellIOReaderAsyncDeinit, - ShellIOWriterAsyncDeinit, - napi_async_work, - ThreadSafeFunction, CppTask, - HotReloadTask, - PollPendingModulesTask, - GetAddrInfoRequestTask, + Exists, + Fchmod, + FChown, + Fdatasync, + FetchTasklet, + Fstat, FSWatchTask, + Fsync, + FTruncate, + Futimes, + GetAddrInfoRequestTask, + HotReloadTask, JSCDeferredWorkTask, - Stat, + Lchmod, + Lchown, + Link, Lstat, - Fstat, + Lutimes, + ManagedTask, + Mkdir, + Mkdtemp, + napi_async_work, + NativeBrotli, + NativeZlib, Open, - ReadFile, - WriteFile, - CopyFile, + PollPendingModulesTask, + ProcessWaiterThreadTask, Read, - Write, - Truncate, - FTruncate, Readdir, ReaddirRecursive, - Close, - Rm, - Rmdir, - Chown, - FChown, - Utimes, - Lutimes, - Chmod, - Fchmod, - Link, - Symlink, + ReadFile, + ReadFileTask, Readlink, - Realpath, - Mkdir, - Fsync, - Fdatasync, - Writev, Readv, + Realpath, Rename, - Access, - AppendFile, - Mkdtemp, - Exists, - Futimes, - Lchmod, - Lchown, - Unlink, - NativeZlib, - NativeBrotli, + Rm, + Rmdir, + RuntimeTranspilerStore, + ServerAllConnectionsClosedTask, + ShellAsync, + ShellAsyncSubprocessDone, + ShellCondExprStatTask, + ShellCpTask, ShellGlobTask, - ShellRmTask, - ShellRmDirTask, - ShellMvCheckTargetTask, - ShellMvBatchedTask, + ShellIOReaderAsyncDeinit, + ShellIOWriterAsyncDeinit, ShellLsTask, ShellMkdirTask, + ShellMvBatchedTask, + ShellMvCheckTargetTask, + ShellRmDirTask, + ShellRmTask, ShellTouchTask, - ShellCpTask, - ShellCondExprStatTask, - ShellAsync, - ShellAsyncSubprocessDone, + Stat, + Symlink, + ThreadSafeFunction, TimerObject, - bun.shell.Interpreter.Builtin.Yes.YesTask, - ProcessWaiterThreadTask, - RuntimeTranspilerStore, - ServerAllConnectionsClosedTask, - bun.bake.DevServer.HotReloadTask, + Truncate, + Unlink, + Utimes, + Write, + WriteFile, + WriteFileTask, + Writev, }); const UnboundedQueue = @import("./unbounded_queue.zig").UnboundedQueue; pub const ConcurrentTask = struct { diff --git a/src/js/node/http.ts b/src/js/node/http.ts index 0674b0c9f4509..8a924eb061a6e 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -301,6 +301,14 @@ var FakeSocket = class Socket extends Duplex { _write(chunk, encoding, callback) {} }; +class ConnResetException extends Error { + constructor(msg) { + super(msg); + this.code = "ECONNRESET"; + this.name = "ConnResetException"; + } +} + const NodeHTTPServerSocket = class Socket extends Duplex { bytesRead = 0; bytesWritten = 0; @@ -317,11 +325,21 @@ const NodeHTTPServerSocket = class Socket extends Duplex { handle.onclose = this.#onClose.bind(this); handle.duplex = this; this.encrypted = encrypted; + this.on("timeout", onNodeHTTPServerSocketTimeout); } - #onClose(callback) { + #onClose() { + const handle = this[kHandle]; this[kHandle] = null; - if ($isCallable(callback)) callback(); + const message = this._httpMessage; + const req = message?.req; + if (req && !req.complete) { + req.destroy(new ConnResetException("aborted")); + } + } + #onCloseForDestroy(closeCallback) { + this.#onClose(); + $isCallable(closeCallback) && closeCallback(); } address() { @@ -338,16 +356,31 @@ const NodeHTTPServerSocket = class Socket extends Duplex { _destroy(err, callback) { const handle = this[kHandle]; - if (!handle) return; // sometimes 'this' is Socket not FakeSocket + if (!handle) { + $isCallable(callback) && callback(err); + return; + } + if (handle.closed) { + const onclose = handle.onclose; + handle.onclose = null; + if ($isCallable(onclose)) { + onclose.$call(handle); + } + $isCallable(callback) && callback(err); + return; + } this[kHandle] = undefined; - handle.onclose = this.#onClose.bind(this, callback); + handle.onclose = this.#onCloseForDestroy.bind(this, callback); handle.close(); } _final(callback) { const handle = this[kHandle]; - if (!handle) return; // sometimes 'this' is Socket not FakeSocket - handle.onclose = this.#onClose.bind(this, callback); + if (!handle) { + callback(); + return; + } + handle.onclose = this.#onCloseForDestroy.bind(this, callback); handle.close(); } @@ -613,26 +646,25 @@ function onRequestEvent(event) { server.emit("timeout", req.socket); break; case NodeHTTPResponseAbortEvent.abort: - emitCloseNTAndComplete(http_res); - emitCloseNTAndComplete(this); http_res[finishedSymbol] = true; + this.destroy(); break; } } } function onServerRequestEvent(this: NodeHTTPServerSocket, event: NodeHTTPResponseAbortEvent) { - const server: Server = this.server; + const server: Server = this?.server; const socket: NodeHTTPServerSocket = this; - switch (event) { case NodeHTTPResponseAbortEvent.abort: { - socket.destroy(); + if (!socket.destroyed) { + socket.destroy(); + } break; } case NodeHTTPResponseAbortEvent.timeout: { socket.emit("timeout"); - server.emit("timeout", socket); break; } } @@ -867,29 +899,28 @@ Server.prototype = { let resolveFunction; let didFinish = false; - http_req.once("error", errorCallback); - http_res.once("error", errorCallback); - - // handle.onabort = onServerRequestEvent.bind(socketHandle); - http_res.once("close", () => { - didFinish = true; - resolveFunction && resolveFunction(); - }); + handle.onabort = onServerRequestEvent.bind(socket); if (isSocketNew) { server.emit("connection", socket); } - socket._httpMessage = http_res; - http_res.socket = socket; socket[kRequest] = http_req; + http_res.assignSocket(socket); + function onClose() { + didFinish = true; + resolveFunction && resolveFunction(); + } + http_res.once("close", onClose); + server.emit("request", http_req, http_res); socket.cork(drainMicrotasks); if (capturedError) { handle = undefined; + http_res.removeListener("close", onClose); if (socket._httpMessage === http_res) { socket._httpMessage = null; } @@ -897,8 +928,8 @@ Server.prototype = { } if (handle.finished || didFinish) { - http_res.off("error", errorCallback); handle = undefined; + http_res.removeListener("close", onClose); if (socket._httpMessage === http_res) { socket._httpMessage = null; } @@ -1070,7 +1101,6 @@ var isNextIncomingMessageHTTPS = false; function emitEOFIncomingMessageOuter(self) { self.push(null); self.complete = true; - process.nextTick(emitCloseNTAndComplete, self); } function emitEOFIncomingMessage(self) { self[eofInProgress] = true; @@ -1108,8 +1138,7 @@ function IncomingMessage(req, defaultIncomingOpts) { this[noBodySymbol] = false; Readable.$call(this); - var { [typeSymbol]: type = NodeHTTPIncomingRequestType.FetchRequest, [reqSymbol]: nodeReq } = - defaultIncomingOpts || {}; + var { [typeSymbol]: type, [reqSymbol]: nodeReq } = defaultIncomingOpts || {}; this[webRequestOrResponse] = req; this[typeSymbol] = type; @@ -1117,10 +1146,14 @@ function IncomingMessage(req, defaultIncomingOpts) { this[statusMessageSymbol] = (req as Response)?.statusText || null; this[statusCodeSymbol] = (req as Response)?.status || 200; - if (!assignHeaders(this, req)) { - this[fakeSocketSymbol] = req; - const reqUrl = String(req?.url || ""); - this.url = reqUrl; + if (type === NodeHTTPIncomingRequestType.FetchRequest || type === NodeHTTPIncomingRequestType.FetchResponse) { + if (!assignHeaders(this, req)) { + this[fakeSocketSymbol] = req; + } + } else { + // Node defaults url and method to null. + this.url = ""; + this.method = null; } this[noBodySymbol] = @@ -1156,6 +1189,10 @@ const IncomingMessagePrototype = { // If there is buffered data, it may trigger 'data' events. // Remove 'data' event listeners explicitly. this.removeAllListeners("data"); + const handle = this[kHandle]; + if (handle) { + handle.ondata = undefined; + } this.resume(); } }, @@ -1176,7 +1213,6 @@ const IncomingMessagePrototype = { return; } else if ((internalRequest = this[kHandle])) { internalRequest.ondata = (chunk, isLast, aborted: NodeHTTPResponseAbortEvent) => { - $debug("ondata", chunk, isLast, aborted); if (aborted === NodeHTTPResponseAbortEvent.abort) { this.destroy(); return; @@ -1240,8 +1276,12 @@ const IncomingMessagePrototype = { var nodeHTTPResponse = this[kHandle]; if (nodeHTTPResponse) { this[kHandle] = undefined; - nodeHTTPResponse.ondata = undefined; + nodeHTTPResponse.onabort = nodeHTTPResponse.ondata = undefined; nodeHTTPResponse.abort(); + const socket = this.socket; + if (socket && !socket.destroyed && this.aborted) { + socket.destroy(err); + } } else { const stream = this[bodyStreamSymbol]; this[bodyStreamSymbol] = undefined; @@ -1257,8 +1297,8 @@ const IncomingMessagePrototype = { socket.destroy(err); } - if (cb && err) { - emitErrorNextTick(this, err, cb); + if ($isCallable(cb)) { + emitErrorNextTickIfErrorListenerNT(this, err, cb); } }, get aborted() { @@ -1564,6 +1604,16 @@ const OutgoingMessagePrototype = { OutgoingMessage.prototype = OutgoingMessagePrototype; $setPrototypeDirect.$call(OutgoingMessage, Stream); +function onNodeHTTPServerSocketTimeout() { + const req = this[kRequest]; + const reqTimeout = req && !req.complete && req.emit("timeout", this); + const res = this._httpMessage; + const resTimeout = res && res.emit("timeout", this); + const serverTimeout = this.server.emit("timeout", this); + + if (!reqTimeout && !resTimeout && !serverTimeout) this.destroy(); +} + function onTimeout() { this[timeoutTimerSymbol] = undefined; this[kAbortController]?.abort(); @@ -1590,10 +1640,9 @@ function emitContinueAndSocketNT(self) { } function emitCloseNT(self) { if (!self._closed) { + self.destroyed = true; self._closed = true; - if (!self.destroyed) { - self.destroy(); - } + self.emit("close"); } } @@ -1630,8 +1679,9 @@ function onServerResponseClose() { // Ergo, we need to deal with stale 'close' events and handle the case // where the ServerResponse object has already been deconstructed. // Fortunately, that requires only a single if check. :-) - if (this._httpMessage) { - emitCloseNT(this._httpMessage); + const httpMessage = this._httpMessage; + if (httpMessage) { + emitCloseNT(httpMessage); } } @@ -1674,7 +1724,6 @@ function callWriteHeadIfObservable(self, headerState) { !(self.writeHead === OriginalWriteHeadFn && self._implicitHeader === OriginalImplicitHeadFn) ) { self.writeHead(self.statusCode, self.statusMessage, self[headersSymbol]); - console.log("called rwriteHead"); } } @@ -1697,7 +1746,7 @@ const ServerResponsePrototype = { // But we don't want it for the fetch() response version. end(chunk, encoding, callback) { if (this.destroyed) { - emitErrorNextTick(this, $ERR_STREAM_DESTROYED("Stream is destroyed"), callback); + emitErrorNextTickIfErrorListenerNT(this, $ERR_STREAM_DESTROYED("Stream is destroyed"), callback); return this; } @@ -1716,7 +1765,7 @@ const ServerResponsePrototype = { } if (isFinished && chunk) { - emitErrorNextTick(this, $ERR_STREAM_WRITE_AFTER_END("Stream is already finished"), callback); + emitErrorNextTickIfErrorListenerNT(this, $ERR_STREAM_WRITE_AFTER_END("Stream is already finished"), callback); return this; } @@ -1744,11 +1793,11 @@ const ServerResponsePrototype = { } this._header = " "; const req = this.req; - const reqClosed = req?._closed; - if (reqClosed === false && req) { - process.nextTick(emitCloseNTAndComplete, req); + const socket = req.socket; + if (!req._consuming && !req?._readableState?.resumeScheduled) { + req._dump(); } - + this.detachSocket(socket); this[finishedSymbol] = this.finished = true; this.emit("prefinish"); @@ -1766,30 +1815,15 @@ const ServerResponsePrototype = { self.emit("error", err); } - process.nextTick(function (self) { - if (!self[closedSymbol]) { - self[closedSymbol] = true; - if (!self.destroyed) { - self.destroy(); - } - self.emit("close"); - } - }, self); + process.nextTick(emitCloseNT, self); }, callback, this, ); } else { this.emit("finish"); - process.nextTick(function (self) { - if (!self[closedSymbol]) { - self[closedSymbol] = true; - if (!self.destroyed) { - self.destroy(); - } - self.emit("close"); - } - }, this); + + process.nextTick(emitCloseNT, this); } } @@ -1818,7 +1852,7 @@ const ServerResponsePrototype = { } if (this.finished) { - emitErrorNextTick(this, $ERR_STREAM_WRITE_AFTER_END("Stream is already finished"), callback); + emitErrorNextTickIfErrorListenerNT(this, $ERR_STREAM_WRITE_AFTER_END("Stream is already finished"), callback); return false; } @@ -1862,6 +1896,15 @@ const ServerResponsePrototype = { OutgoingMessage.prototype._finish.$call(this); }, + detachSocket(socket) { + if (socket._httpMessage === this) { + socket.removeListener("close", onServerResponseClose); + socket._httpMessage = null; + } + + this.socket = null; + }, + _implicitHeader() { // @ts-ignore this.writeHead(this.statusCode); @@ -1919,7 +1962,7 @@ const ServerResponsePrototype = { throw ERR_HTTP_SOCKET_ASSIGNED(); } socket._httpMessage = this; - socket.once("close", () => onServerResponseClose.$call(socket)); + socket.once("close", onServerResponseClose); this.socket = socket; this.emit("socket", socket); }, @@ -2219,7 +2262,7 @@ class ClientRequest extends (OutgoingMessage as unknown as typeof import("node:h if (chunk) { if (this[finishedSymbol]) { - emitErrorNextTick(this, $ERR_STREAM_WRITE_AFTER_END("Cannot write after end"), callback); + emitErrorNextTickIfErrorListenerNT(this, $ERR_STREAM_WRITE_AFTER_END("Cannot write after end"), callback); return this; } @@ -3043,14 +3086,22 @@ function onError(self, error, cb) { if ($isCallable(cb)) { cb(error); } +} - if (typeof self.emit === "function" && !self._closed) { - self.emit("error", error); - } +function emitErrorNextTickIfErrorListenerNT(self, err, cb) { + process.nextTick(emitErrorNextTickIfErrorListener, self, err, cb); } -function emitErrorNextTick(self, err, cb) { - process.nextTick(onError, self, err, cb); +function emitErrorNextTickIfErrorListener(self, err, cb) { + if ($isCallable(cb)) { + // This is to keep backward compatible behavior. + // An error is emitted only if there are listeners attached to the event. + if (self.listenerCount("error") == 0) { + cb(); + } else { + cb(err); + } + } } function emitAbortNextTick(self) { diff --git a/test/js/node/http/node-http.test.ts b/test/js/node/http/node-http.test.ts index b394036684e79..1d2674f504756 100644 --- a/test/js/node/http/node-http.test.ts +++ b/test/js/node/http/node-http.test.ts @@ -267,10 +267,20 @@ describe("node:http", () => { } if (reqUrl.pathname.includes("timeout")) { if (timer) clearTimeout(timer); + req.on("timeout", () => { + console.log("req timeout"); + }); + res.on("timeout", () => { + console.log("res timeout"); + }); timer = setTimeout(() => { + if (res.closed) { + return; + } + res.end("Hello World"); timer = null; - }, 3000); + }, 3000).unref(); return; } if (reqUrl.pathname === "/pathTest") { @@ -1357,417 +1367,6 @@ it("should not accept untrusted certificates", async () => { server.close(); }); -it("IncomingMessage with a RequestLike object", () => { - const rawHeadersMap = { - "x-test": "test", - "Real-Header": "test", - "content-type": "text/plain", - "User-Agent": "Bun", - }; - - // To excercise the case where inline capacity cannot be used - for (let i = 0; i < 64; i++) { - rawHeadersMap[`header-${i}`] = `value-${i}`; - } - - const headers = new Headers(rawHeadersMap); - headers.append("set-cookie", "foo=bar"); - headers.append("set-cookie", "bar=baz"); - - const request = new Request("https://example.com/hello/hi", { - headers, - }); - - const incomingMessageFromRequest = new IncomingMessage(request); - const incomingMessageFromRequestLike1 = new IncomingMessage({ - url: "/hello/hi", - headers: headers, - method: request.method, - }); - const incomingMessageFromRequestLike2 = new IncomingMessage({ - url: "/hello/hi", - headers: headers.toJSON(), - method: request.method, - }); - for (let incomingMessageFromRequestLike of [ - incomingMessageFromRequestLike1, - incomingMessageFromRequestLike2, - incomingMessageFromRequest, - ]) { - expect(incomingMessageFromRequestLike.headers).toEqual(incomingMessageFromRequest.headers); - expect(incomingMessageFromRequestLike.method).toEqual(incomingMessageFromRequest.method); - expect(incomingMessageFromRequestLike.url).toEqual(incomingMessageFromRequest.url); - expect(incomingMessageFromRequestLike.headers).toEqual({ - "x-test": "test", - "real-header": "test", - "content-type": "text/plain", - "user-agent": "Bun", - "set-cookie": ["foo=bar", "bar=baz"], - "header-0": "value-0", - "header-1": "value-1", - "header-10": "value-10", - "header-11": "value-11", - "header-12": "value-12", - "header-13": "value-13", - "header-14": "value-14", - "header-15": "value-15", - "header-16": "value-16", - "header-17": "value-17", - "header-18": "value-18", - "header-19": "value-19", - "header-2": "value-2", - "header-20": "value-20", - "header-21": "value-21", - "header-22": "value-22", - "header-23": "value-23", - "header-24": "value-24", - "header-25": "value-25", - "header-26": "value-26", - "header-27": "value-27", - "header-28": "value-28", - "header-29": "value-29", - "header-3": "value-3", - "header-30": "value-30", - "header-31": "value-31", - "header-32": "value-32", - "header-33": "value-33", - "header-34": "value-34", - "header-35": "value-35", - "header-36": "value-36", - "header-37": "value-37", - "header-38": "value-38", - "header-39": "value-39", - "header-4": "value-4", - "header-40": "value-40", - "header-41": "value-41", - "header-42": "value-42", - "header-43": "value-43", - "header-44": "value-44", - "header-45": "value-45", - "header-46": "value-46", - "header-47": "value-47", - "header-48": "value-48", - "header-49": "value-49", - "header-5": "value-5", - "header-50": "value-50", - "header-51": "value-51", - "header-52": "value-52", - "header-53": "value-53", - "header-54": "value-54", - "header-55": "value-55", - "header-56": "value-56", - "header-57": "value-57", - "header-58": "value-58", - "header-59": "value-59", - "header-6": "value-6", - "header-60": "value-60", - "header-61": "value-61", - "header-62": "value-62", - "header-63": "value-63", - "header-7": "value-7", - "header-8": "value-8", - "header-9": "value-9", - }); - } - - // this one preserves the original case - expect(incomingMessageFromRequestLike1.rawHeaders).toEqual([ - "content-type", - "text/plain", - "user-agent", - "Bun", - "set-cookie", - "foo=bar", - "set-cookie", - "bar=baz", - "x-test", - "test", - "Real-Header", - "test", - "header-0", - "value-0", - "header-1", - "value-1", - "header-2", - "value-2", - "header-3", - "value-3", - "header-4", - "value-4", - "header-5", - "value-5", - "header-6", - "value-6", - "header-7", - "value-7", - "header-8", - "value-8", - "header-9", - "value-9", - "header-10", - "value-10", - "header-11", - "value-11", - "header-12", - "value-12", - "header-13", - "value-13", - "header-14", - "value-14", - "header-15", - "value-15", - "header-16", - "value-16", - "header-17", - "value-17", - "header-18", - "value-18", - "header-19", - "value-19", - "header-20", - "value-20", - "header-21", - "value-21", - "header-22", - "value-22", - "header-23", - "value-23", - "header-24", - "value-24", - "header-25", - "value-25", - "header-26", - "value-26", - "header-27", - "value-27", - "header-28", - "value-28", - "header-29", - "value-29", - "header-30", - "value-30", - "header-31", - "value-31", - "header-32", - "value-32", - "header-33", - "value-33", - "header-34", - "value-34", - "header-35", - "value-35", - "header-36", - "value-36", - "header-37", - "value-37", - "header-38", - "value-38", - "header-39", - "value-39", - "header-40", - "value-40", - "header-41", - "value-41", - "header-42", - "value-42", - "header-43", - "value-43", - "header-44", - "value-44", - "header-45", - "value-45", - "header-46", - "value-46", - "header-47", - "value-47", - "header-48", - "value-48", - "header-49", - "value-49", - "header-50", - "value-50", - "header-51", - "value-51", - "header-52", - "value-52", - "header-53", - "value-53", - "header-54", - "value-54", - "header-55", - "value-55", - "header-56", - "value-56", - "header-57", - "value-57", - "header-58", - "value-58", - "header-59", - "value-59", - "header-60", - "value-60", - "header-61", - "value-61", - "header-62", - "value-62", - "header-63", - "value-63", - ]); - - // this one does not preserve the original case - expect(incomingMessageFromRequestLike2.rawHeaders).toEqual([ - "content-type", - "text/plain", - "user-agent", - "Bun", - "set-cookie", - "foo=bar", - "set-cookie", - "bar=baz", - "x-test", - "test", - "real-header", - "test", - "header-0", - "value-0", - "header-1", - "value-1", - "header-2", - "value-2", - "header-3", - "value-3", - "header-4", - "value-4", - "header-5", - "value-5", - "header-6", - "value-6", - "header-7", - "value-7", - "header-8", - "value-8", - "header-9", - "value-9", - "header-10", - "value-10", - "header-11", - "value-11", - "header-12", - "value-12", - "header-13", - "value-13", - "header-14", - "value-14", - "header-15", - "value-15", - "header-16", - "value-16", - "header-17", - "value-17", - "header-18", - "value-18", - "header-19", - "value-19", - "header-20", - "value-20", - "header-21", - "value-21", - "header-22", - "value-22", - "header-23", - "value-23", - "header-24", - "value-24", - "header-25", - "value-25", - "header-26", - "value-26", - "header-27", - "value-27", - "header-28", - "value-28", - "header-29", - "value-29", - "header-30", - "value-30", - "header-31", - "value-31", - "header-32", - "value-32", - "header-33", - "value-33", - "header-34", - "value-34", - "header-35", - "value-35", - "header-36", - "value-36", - "header-37", - "value-37", - "header-38", - "value-38", - "header-39", - "value-39", - "header-40", - "value-40", - "header-41", - "value-41", - "header-42", - "value-42", - "header-43", - "value-43", - "header-44", - "value-44", - "header-45", - "value-45", - "header-46", - "value-46", - "header-47", - "value-47", - "header-48", - "value-48", - "header-49", - "value-49", - "header-50", - "value-50", - "header-51", - "value-51", - "header-52", - "value-52", - "header-53", - "value-53", - "header-54", - "value-54", - "header-55", - "value-55", - "header-56", - "value-56", - "header-57", - "value-57", - "header-58", - "value-58", - "header-59", - "value-59", - "header-60", - "value-60", - "header-61", - "value-61", - "header-62", - "value-62", - "header-63", - "value-63", - ]); -}); - -it("#6892", () => { - const totallyValid = ["*", "/", "/foo", "/foo/bar"]; - for (const url of totallyValid) { - const req = new IncomingMessage({ url }); - expect(req.url).toBe(url); - expect(req.method).toBeNull(); - } -}); - it("#4415.1 ServerResponse es6", () => { class Response extends ServerResponse { constructor(req) { @@ -1806,10 +1405,43 @@ it("#4415.3 Server es5", done => { }); }); -it("#4415.4 IncomingMessage es5", () => { +it("#4415.4 IncomingMessage es5", done => { + // This matches Node.js: const im = Object.create(IncomingMessage.prototype); IncomingMessage.call(im, { url: "/foo" }); - expect(im.url).toBe("/foo"); + expect(im.url).toBe(""); + + let didCall = false; + function Subclass(...args) { + IncomingMessage.apply(this, args); + didCall = true; + } + Object.setPrototypeOf(Subclass.prototype, IncomingMessage.prototype); + Object.setPrototypeOf(Subclass, IncomingMessage); + + const server = new Server( + { + IncomingMessage: Subclass, + }, + (req, res) => { + if (req instanceof Subclass && didCall) { + expect(req.url).toBe("/foo"); + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("hello"); + } else { + res.writeHead(500, { "Content-Type": "text/plain" }); + res.end("bye"); + } + }, + ); + server.listen(0, () => { + fetch(`http://localhost:${server.address().port}/foo`, { + method: "GET", + }).then(response => { + expect(response.status).toBe(200); + server.close(done); + }); + }); }); it("#9242.1 Server has constructor", () => { @@ -2339,6 +1971,7 @@ it("should emit close, and complete should be true only after close #13373", asy it("should emit close when connection is aborted", async () => { const server = http.createServer().listen(0); + server.unref(); try { await once(server, "listening"); const controller = new AbortController(); @@ -2347,11 +1980,13 @@ it("should emit close when connection is aborted", async () => { .catch(() => {}); const [req, res] = await once(server, "request"); - expect(req.complete).toBe(false); - const closeEvent = once(req, "close"); + const closeEvent = Promise.withResolvers(); + req.once("close", () => { + closeEvent.resolve(); + }); controller.abort(); - await closeEvent; - expect(req.complete).toBe(true); + await closeEvent.promise; + expect(req.aborted).toBe(true); } finally { server.close(); } @@ -2368,7 +2003,7 @@ it("should emit timeout event", async () => { const [req, res] = await once(server, "request"); expect(req.complete).toBe(false); let callBackCalled = false; - req.setTimeout(1000, () => { + req.setTimeout(100, () => { callBackCalled = true; }); await once(req, "timeout"); @@ -2383,7 +2018,7 @@ it("should emit timeout event when using server.setTimeout", async () => { try { await once(server, "listening"); let callBackCalled = false; - server.setTimeout(1000, () => { + server.setTimeout(100, () => { callBackCalled = true; console.log("Called timeout"); }); diff --git a/test/js/third_party/body-parser/express-body-parser-test.test.ts b/test/js/third_party/body-parser/express-body-parser-test.test.ts index 841d2f8c1dcce..99e2fed2e2125 100644 --- a/test/js/third_party/body-parser/express-body-parser-test.test.ts +++ b/test/js/third_party/body-parser/express-body-parser-test.test.ts @@ -35,9 +35,21 @@ test("httpServer", async () => { }); app.use(json()); + let closeCount = 0; + let responseCloseCount = 0; var reached = false; // This throws a TypeError since it uses body-parser.json app.post("/ping", (request: Request, response: Response) => { + request.on("close", () => { + if (closeCount++ === 1) { + throw new Error("request Close called multiple times"); + } + }); + response.on("close", () => { + if (responseCloseCount++ === 1) { + throw new Error("response Close called multiple times"); + } + }); expect(request.body).toEqual({ hello: "world" }); expect(request.query).toStrictEqual({ hello: "123", From c24ffec53feed8ea312e44a093f0a22c5e13a473 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 14 Oct 2024 06:16:14 -0700 Subject: [PATCH 19/36] Fake backpressure --- src/js/node/http.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/js/node/http.ts b/src/js/node/http.ts index 8a924eb061a6e..f288ab858d54e 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -2233,19 +2233,35 @@ class ClientRequest extends (OutgoingMessage as unknown as typeof import("node:h (typeof chunk === "string" && (encoding === "utf-8" || encoding === "utf8" || !encoding)) || // Buffer ($isTypedArrayView(chunk) && (!encoding || encoding === "buffer" || encoding === "utf-8")); - + let bodySize = 0; if (!canSkipReEncodingData) { chunk = Buffer.from(chunk, encoding); + bodySize = chunk.length; + } else { + bodySize = chunk.length; } if (!this.#bodyChunks) { this.#bodyChunks = [chunk]; + if (callback) callback(); return true; } + + // Signal fake backpressure if the body size is > 1024 * 1024 + // So that code which loops forever until backpressure is signaled + // will eventually exit. + for (let chunk of this.#bodyChunks) { + bodySize += chunk.length; + if (bodySize > 1024 * 1024) { + break; + } + } + this.#bodyChunks.push(chunk); + if (callback) callback(); - return true; + return bodySize < 128 * 1024; } end(chunk, encoding, callback) { From ca1567e6f3c6329cc05be2c0235548f8f99f0c5b Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 14 Oct 2024 06:23:33 -0700 Subject: [PATCH 20/36] Fix broken test --- src/js/node/http.ts | 7 ++++--- test/js/web/fetch/client-fetch.test.ts | 23 +++++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/js/node/http.ts b/src/js/node/http.ts index f288ab858d54e..02d4efee78072 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -2228,6 +2228,7 @@ class ClientRequest extends (OutgoingMessage as unknown as typeof import("node:h } #write(chunk, encoding, callback) { + const MAX_FAKE_BACKPRESSURE_SIZE = 1024 * 1024; const canSkipReEncodingData = // UTF-8 string: (typeof chunk === "string" && (encoding === "utf-8" || encoding === "utf8" || !encoding)) || @@ -2251,17 +2252,17 @@ class ClientRequest extends (OutgoingMessage as unknown as typeof import("node:h // Signal fake backpressure if the body size is > 1024 * 1024 // So that code which loops forever until backpressure is signaled // will eventually exit. + for (let chunk of this.#bodyChunks) { bodySize += chunk.length; - if (bodySize > 1024 * 1024) { + if (bodySize > MAX_FAKE_BACKPRESSURE_SIZE) { break; } } - this.#bodyChunks.push(chunk); if (callback) callback(); - return bodySize < 128 * 1024; + return bodySize < MAX_FAKE_BACKPRESSURE_SIZE; } end(chunk, encoding, callback) { diff --git a/test/js/web/fetch/client-fetch.test.ts b/test/js/web/fetch/client-fetch.test.ts index bf6d19e3285bd..b4064f0a9535f 100644 --- a/test/js/web/fetch/client-fetch.test.ts +++ b/test/js/web/fetch/client-fetch.test.ts @@ -287,17 +287,20 @@ test("redirect with body", async () => { let count = 0; await using server = createServer(async (req, res) => { let body = ""; - for await (const chunk of req) { + req.on("data", chunk => { body += chunk; - } - expect(body).toBe("asd"); - if (count++ === 0) { - res.setHeader("location", "asd"); - res.statusCode = 302; - res.end(); - } else { - res.end(String(count)); - } + }); + + req.on("end", () => { + expect(body).toBe("asd"); + if (count++ === 0) { + res.setHeader("location", "asd"); + res.statusCode = 302; + res.end(); + } else { + res.end(String(count)); + } + }); }).listen(0); await once(server, "listening"); From 240e795224c91e983ae7644f9e36208d52c3e208 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 14 Oct 2024 06:29:13 -0700 Subject: [PATCH 21/36] Update BunObject.cpp --- src/bun.js/bindings/BunObject.cpp | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index 48147ef93e66d..49f08a6ca81e2 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -560,32 +560,6 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj return JSC::JSValue::encode(JSC::jsString(vm, fileSystemPath)); } -JSC_DEFINE_HOST_FUNCTION(functionFork, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) -{ - auto* globalObject = defaultGlobalObject(lexicalGlobalObject); - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - JSC::Strong strongCallback(vm, callFrame->argument(0).getObject()); - - globalObject->scriptExecutionContext()->postTask([strongCallback = WTFMove(strongCallback)](auto& context) { - auto pid = fork(); - if (pid > 0) { - waitpid(pid, nullptr, 0); - } - printf("post-fork%d\n", pid); - auto* function = strongCallback.get(); - auto* globalObject = context.globalObject(); - JSC::MarkedArgumentBuffer arguments; - arguments.append(jsNumber(pid)); - WTF::NakedPtr exception; - JSC::call(globalObject, function, JSC::getCallData(function), JSC::jsUndefined(), arguments, exception); - if (UNLIKELY(exception)) { - } - }); - - return JSValue::encode(jsUndefined()); -} - /* Source for BunObject.lut.h @begin bunObjectTable $ constructBunShell ReadOnly|DontDelete|PropertyCallback @@ -615,7 +589,6 @@ JSC_DEFINE_HOST_FUNCTION(functionFork, (JSC::JSGlobalObject * lexicalGlobalObjec deepEquals functionBunDeepEquals DontDelete|Function 2 deepMatch functionBunDeepMatch DontDelete|Function 2 deflateSync BunObject_callback_deflateSync DontDelete|Function 1 - fork functionFork DontDelete|Function 1 dns constructDNSObject ReadOnly|DontDelete|PropertyCallback enableANSIColors BunObject_getter_wrap_enableANSIColors DontDelete|PropertyCallback env constructEnvObject ReadOnly|DontDelete|PropertyCallback From 3446bce0929ecf90210d33e1ebaac8107e2a62aa Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 14 Oct 2024 06:48:07 -0700 Subject: [PATCH 22/36] Avoid abort when the response is finished --- src/js/node/http.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/js/node/http.ts b/src/js/node/http.ts index 02d4efee78072..67158f32c9331 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -1277,7 +1277,9 @@ const IncomingMessagePrototype = { if (nodeHTTPResponse) { this[kHandle] = undefined; nodeHTTPResponse.onabort = nodeHTTPResponse.ondata = undefined; - nodeHTTPResponse.abort(); + if (!nodeHTTPResponse.finished) { + nodeHTTPResponse.abort(); + } const socket = this.socket; if (socket && !socket.destroyed && this.aborted) { socket.destroy(err); From 81c007ccbe0785d21a37f65cd86c5dfd926f7cbd Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 14 Oct 2024 20:53:44 -0700 Subject: [PATCH 23/36] Update http.ts --- src/js/node/http.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/js/node/http.ts b/src/js/node/http.ts index 67158f32c9331..c5e6f2f119074 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -1260,8 +1260,10 @@ const IncomingMessagePrototype = { _finish() { this.emit("prefinish"); }, - _destroy(err, cb) { - if (!this.readableEnded || !this.complete) { + _destroy: function IncomingMessage_destroy(err, cb) { + const shouldEmitAborted = !this.readableEnded || !this.complete; + + if (shouldEmitAborted) { this[abortedSymbol] = true; // IncomingMessage emits 'aborted'. // Client emits 'abort'. @@ -1281,7 +1283,7 @@ const IncomingMessagePrototype = { nodeHTTPResponse.abort(); } const socket = this.socket; - if (socket && !socket.destroyed && this.aborted) { + if (socket && !socket.destroyed && shouldEmitAborted) { socket.destroy(err); } } else { @@ -1292,11 +1294,11 @@ const IncomingMessagePrototype = { if (streamState === $streamReadable || streamState === $streamWaiting || streamState === $streamWritable) { stream?.cancel?.().catch(nop); } - } - const socket = this[fakeSocketSymbol]; - if (socket) { - socket.destroy(err); + const socket = this[fakeSocketSymbol]; + if (socket && !socket.destroyed && shouldEmitAborted) { + socket.destroy(err); + } } if ($isCallable(cb)) { From efd0862b61b17bb82b843d26f2412b759c9769ab Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 14 Oct 2024 22:59:10 -0700 Subject: [PATCH 24/36] Zlib micro optimization --- src/bun.js/node/node_zlib_binding.zig | 49 +++++++++++++++++---------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/src/bun.js/node/node_zlib_binding.zig b/src/bun.js/node/node_zlib_binding.zig index 5337a6427146b..c68598ad37f84 100644 --- a/src/bun.js/node/node_zlib_binding.zig +++ b/src/bun.js/node/node_zlib_binding.zig @@ -114,35 +114,25 @@ pub fn CompressionStream(comptime T: type) type { // const vm = globalThis.bunVM(); - var task = AsyncJob.new(.{ - .binding = this, - }); + this.task = .{ .callback = &AsyncJob.runTask }; this.poll_ref.ref(vm); - JSC.WorkPool.schedule(&task.task); + JSC.WorkPool.schedule(&this.task); return .undefined; } const AsyncJob = struct { - task: JSC.WorkPoolTask = .{ .callback = &runTask }, - binding: *T, - - pub usingnamespace bun.New(@This()); - - pub fn runTask(this: *JSC.WorkPoolTask) void { - var job: *AsyncJob = @fieldParentPtr("task", this); - job.run(); - job.destroy(); + pub fn runTask(task: *JSC.WorkPoolTask) void { + const this: *T = @fieldParentPtr("task", task); + AsyncJob.run(this); } - pub fn run(job: *AsyncJob) void { - const this = job.binding; + pub fn run(this: *T) void { const globalThis: *JSC.JSGlobalObject = this.globalThis; const vm = globalThis.bunVMConcurrently(); this.stream.doWork(); - this.poll_ref.refConcurrently(vm); vm.enqueueTaskConcurrent(JSC.ConcurrentTask.create(JSC.Task.init(this))); } }; @@ -294,6 +284,25 @@ pub fn CompressionStream(comptime T: type) type { pub const NativeZlib = JSC.Codegen.JSNativeZlib.getConstructor; +const CountedKeepAlive = struct { + keep_alive: bun.Async.KeepAlive = .{}, + ref_count: u32 = 0, + + pub fn ref(this: *@This(), vm: *JSC.VirtualMachine) void { + if (this.ref_count == 0) { + this.keep_alive.ref(vm); + } + this.ref_count += 1; + } + + pub fn unref(this: *@This(), vm: *JSC.VirtualMachine) void { + this.ref_count -= 1; + if (this.ref_count == 0) { + this.keep_alive.unref(vm); + } + } +}; + pub const SNativeZlib = struct { pub usingnamespace bun.NewRefCounted(@This(), deinit); pub usingnamespace JSC.Codegen.JSNativeZlib; @@ -306,11 +315,12 @@ pub const SNativeZlib = struct { write_result: ?[*]u32 = null, write_callback: JSC.Strong = .{}, onerror_value: JSC.Strong = .{}, - poll_ref: bun.Async.KeepAlive = .{}, + poll_ref: CountedKeepAlive = .{}, this_value: JSC.Strong = .{}, write_in_progress: bool = false, pending_close: bool = false, closed: bool = false, + task: JSC.WorkPoolTask = .{ .callback = undefined }, pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) ?*@This() { const arguments = callframe.argumentsUndef(4).ptr; @@ -662,11 +672,14 @@ pub const SNativeBrotli = struct { write_result: ?[*]u32 = null, write_callback: JSC.Strong = .{}, onerror_value: JSC.Strong = .{}, - poll_ref: bun.Async.KeepAlive = .{}, + poll_ref: CountedKeepAlive = .{}, this_value: JSC.Strong = .{}, write_in_progress: bool = false, pending_close: bool = false, closed: bool = false, + task: JSC.WorkPoolTask = .{ + .callback = undefined, + }, pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) ?*@This() { const arguments = callframe.argumentsUndef(1).ptr; From 6a744fced6b9de59e57de77bc34dd1450d641e6f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 14 Oct 2024 23:02:20 -0700 Subject: [PATCH 25/36] Update libuwsockets.cpp --- src/deps/libuwsockets.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/deps/libuwsockets.cpp b/src/deps/libuwsockets.cpp index 69da92812deed..b655cfd2c6b4e 100644 --- a/src/deps/libuwsockets.cpp +++ b/src/deps/libuwsockets.cpp @@ -1254,6 +1254,17 @@ extern "C" } } +extern "C" void uws_res_clear_corked_socket(us_loop_t *loop) { + uWS::LoopData *loopData = uWS::AsyncSocket::getLoopData(loop); + if (loopData->getCorkedSocket()) { + if (loopData->isCorkedSSL()) { + ((uWS::AsyncSocket *) loopData->getCorkedSocket())->uncork(); + } else { + ((uWS::AsyncSocket *) loopData->getCorkedSocket())->uncork(); + } + } +} + bool uws_res_write(int ssl, uws_res_r res, const char *data, size_t *length) nonnull_fn_decl; bool uws_res_write(int ssl, uws_res_r res, const char *data, size_t *length) From 029e5bdb7c1b3f511617c962475a7162c8bb19e3 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 15 Oct 2024 00:03:34 -0700 Subject: [PATCH 26/36] Update event_loop.zig --- src/bun.js/event_loop.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index f55647b01da3f..728d6298c5e3d 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -915,6 +915,7 @@ pub const EventLoop = struct { } while (@field(this, queue_name).readItem()) |task| { + log("run {s}", .{@tagName(task.tag())}); defer counter += 1; switch (task.tag()) { @field(Task.Tag, typeBaseName(@typeName(ShellAsync))) => { From 70ce87503a0079e54bf685ada3a914c529ac8640 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 15 Oct 2024 00:06:32 -0700 Subject: [PATCH 27/36] Revert "Update libuwsockets.cpp" This reverts commit 6a744fced6b9de59e57de77bc34dd1450d641e6f. --- src/deps/libuwsockets.cpp | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/deps/libuwsockets.cpp b/src/deps/libuwsockets.cpp index b655cfd2c6b4e..69da92812deed 100644 --- a/src/deps/libuwsockets.cpp +++ b/src/deps/libuwsockets.cpp @@ -1254,17 +1254,6 @@ extern "C" } } -extern "C" void uws_res_clear_corked_socket(us_loop_t *loop) { - uWS::LoopData *loopData = uWS::AsyncSocket::getLoopData(loop); - if (loopData->getCorkedSocket()) { - if (loopData->isCorkedSSL()) { - ((uWS::AsyncSocket *) loopData->getCorkedSocket())->uncork(); - } else { - ((uWS::AsyncSocket *) loopData->getCorkedSocket())->uncork(); - } - } -} - bool uws_res_write(int ssl, uws_res_r res, const char *data, size_t *length) nonnull_fn_decl; bool uws_res_write(int ssl, uws_res_r res, const char *data, size_t *length) From dd0d8ac272ba4570b6a1b1245d98908f0dc47ca0 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 15 Oct 2024 11:09:35 -0700 Subject: [PATCH 28/36] A few experiments to reduce time spent in spin loops on other threads --- .../bun-usockets/src/eventing/epoll_kqueue.c | 12 +++++++++++- packages/bun-usockets/src/internal/loop_data.h | 1 + packages/bun-usockets/src/loop.c | 1 + src/bun.js/bindings/BunJSCEventLoop.cpp | 18 ++++++++++++++++++ src/bun.js/bindings/bindings.zig | 16 ++++++++++++++++ src/bun.js/event_loop.zig | 1 + src/bun.js/module_loader.zig | 2 ++ src/deps/uws.zig | 1 + 8 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 src/bun.js/bindings/BunJSCEventLoop.cpp diff --git a/packages/bun-usockets/src/eventing/epoll_kqueue.c b/packages/bun-usockets/src/eventing/epoll_kqueue.c index 9395e190ea0a2..5a22601d68dbd 100644 --- a/packages/bun-usockets/src/eventing/epoll_kqueue.c +++ b/packages/bun-usockets/src/eventing/epoll_kqueue.c @@ -230,6 +230,8 @@ void us_loop_run(struct us_loop_t *loop) { } } +extern int Bun__JSC_onBeforeWait(void*); +extern void Bun__JSC_onAfterWait(void*); void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout) { if (loop->num_polls == 0) @@ -246,9 +248,13 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout /* Emit pre callback */ us_internal_loop_pre(loop); + int needs_after_wait = 0; + if (loop->data.jsc_vm) { + needs_after_wait = Bun__JSC_onBeforeWait(loop->data.jsc_vm); + } + /* Fetch ready polls */ #ifdef LIBUS_USE_EPOLL - loop->num_ready_polls = bun_epoll_pwait2(loop->fd, loop->ready_polls, 1024, timeout); #else do { @@ -256,6 +262,10 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout } while (IS_EINTR(loop->num_ready_polls)); #endif + if (needs_after_wait) { + Bun__JSC_onAfterWait(loop->data.jsc_vm); + } + /* Iterate ready polls, dispatching them by type */ for (loop->current_ready_poll = 0; loop->current_ready_poll < loop->num_ready_polls; loop->current_ready_poll++) { struct us_poll_t *poll = GET_READY_POLL(loop, loop->current_ready_poll); diff --git a/packages/bun-usockets/src/internal/loop_data.h b/packages/bun-usockets/src/internal/loop_data.h index 1f0a3adb76790..ddbaee9ffc76b 100644 --- a/packages/bun-usockets/src/internal/loop_data.h +++ b/packages/bun-usockets/src/internal/loop_data.h @@ -44,6 +44,7 @@ struct us_internal_loop_data_t { char parent_tag; /* We do not care if this flips or not, it doesn't matter */ size_t iteration_nr; + void* jsc_vm; }; #endif // LOOP_DATA_H diff --git a/packages/bun-usockets/src/loop.c b/packages/bun-usockets/src/loop.c index 9bd315822b7b0..e4b7845f239e9 100644 --- a/packages/bun-usockets/src/loop.c +++ b/packages/bun-usockets/src/loop.c @@ -48,6 +48,7 @@ void us_internal_loop_data_init(struct us_loop_t *loop, void (*wakeup_cb)(struct loop->data.parent_tag = 0; loop->data.closed_context_head = 0; + loop->data.jsc_vm = 0; loop->data.wakeup_async = us_internal_create_async(loop, 1, 0); us_internal_async_set(loop->data.wakeup_async, (void (*)(struct us_internal_async *)) wakeup_cb); diff --git a/src/bun.js/bindings/BunJSCEventLoop.cpp b/src/bun.js/bindings/BunJSCEventLoop.cpp new file mode 100644 index 0000000000000..7a3cf95b2a01e --- /dev/null +++ b/src/bun.js/bindings/BunJSCEventLoop.cpp @@ -0,0 +1,18 @@ +#include "root.h" + +#include +#include + +extern "C" int Bun__JSC_onBeforeWait(JSC::VM* vm) +{ + if (vm->heap.hasAccess()) { + vm->heap.releaseAccess(); + return 1; + } + return 0; +} + +extern "C" void Bun__JSC_onAfterWait(JSC::VM* vm) +{ + vm->heap.acquireAccess(); +} diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index f6e0068dde1b7..03b71f06a2f2b 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -6163,6 +6163,22 @@ pub const VM = extern struct { LargeHeap = 1, }; + extern fn Bun__JSC_onBeforeWait(vm: *VM) i32; + extern fn Bun__JSC_onAfterWait(vm: *VM) void; + pub const ReleaseHeapAccess = struct { + vm: *VM, + needs_to_release: bool, + pub fn acquire(this: *const ReleaseHeapAccess) void { + if (this.needs_to_release) { + Bun__JSC_onAfterWait(this.vm); + } + } + }; + + pub fn releaseHeapAccess(vm: *VM) ReleaseHeapAccess { + return .{ .vm = vm, .needs_to_release = Bun__JSC_onBeforeWait(vm) != 0 }; + } + pub fn create(heap_type: HeapType) *VM { return cppFn("create", .{@intFromEnum(heap_type)}); } diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 728d6298c5e3d..aeb9b18d3a82c 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -545,6 +545,7 @@ pub const GarbageCollectionController = struct { const actual = uws.Loop.get(); this.gc_timer = uws.Timer.createFallthrough(actual, this); this.gc_repeating_timer = uws.Timer.createFallthrough(actual, this); + actual.internal_loop_data.jsc_vm = vm.jsc; if (comptime Environment.isDebug) { if (bun.getenvZ("BUN_TRACK_LAST_FN_NAME") != null) { diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index edab5fba41ea3..1bcaa39d770f8 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -1646,6 +1646,8 @@ pub const ModuleLoader = struct { var parse_result: ParseResult = switch (disable_transpilying or (loader == .json and !path.isJSONCFile())) { inline else => |return_file_only| brk: { + const heap_access = if (!disable_transpilying) jsc_vm.jsc.releaseHeapAccess() else JSC.VM.ReleaseHeapAccess{ .vm = jsc_vm.jsc, .needs_to_release = false }; + defer heap_access.acquire(); break :brk jsc_vm.bundler.parseMaybeReturnFileOnly( parse_options, null, diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 537693e36f5ad..ce6f306064a35 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -59,6 +59,7 @@ pub const InternalLoopData = extern struct { parent_ptr: ?*anyopaque, parent_tag: c_char, iteration_nr: usize, + jsc_vm: ?*JSC.VM, pub fn recvSlice(this: *InternalLoopData) []u8 { return this.recv_buf[0..LIBUS_RECV_BUFFER_LENGTH]; From 083a79cd8e188a97f13cf14745d3c8591f6676e1 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 15 Oct 2024 14:57:54 -0700 Subject: [PATCH 29/36] auto cork --- packages/bun-uws/src/Loop.h | 14 +++++++ packages/bun-uws/src/LoopData.h | 1 + src/bun.js/bindings/webcore/JSTextEncoder.cpp | 10 +---- src/bun.js/event_loop.zig | 29 ++++++++------ src/bun.js/javascript.zig | 2 +- src/bun.js/webcore/encoding.zig | 8 ++-- src/deps/libuwsockets.cpp | 22 +++++++++++ src/deps/uws.zig | 9 +++++ src/js/node/zlib.ts | 39 +++++++++++++------ 9 files changed, 99 insertions(+), 35 deletions(-) diff --git a/packages/bun-uws/src/Loop.h b/packages/bun-uws/src/Loop.h index f8ea7f6e3aacb..4f1a31cb1e670 100644 --- a/packages/bun-uws/src/Loop.h +++ b/packages/bun-uws/src/Loop.h @@ -24,6 +24,7 @@ #include "LoopData.h" #include #include +#include "AsyncSocket.h" extern "C" int bun_is_exiting(); @@ -52,6 +53,15 @@ struct Loop { for (auto &p : loopData->preHandlers) { p.second((Loop *) loop); } + + void *corkedSocket = loopData->getCorkedSocket(); + if (corkedSocket) { + if (loopData->isCorkedSSL()) { + ((uWS::AsyncSocket *) corkedSocket)->uncork(); + } else { + ((uWS::AsyncSocket *) corkedSocket)->uncork(); + } + } } static void postCb(us_loop_t *loop) { @@ -148,6 +158,10 @@ struct Loop { getLazyLoop().loop = nullptr; } + static LoopData* data(struct us_loop_t *loop) { + return (LoopData *) us_loop_ext(loop); + } + void addPostHandler(void *key, MoveOnlyFunction &&handler) { LoopData *loopData = (LoopData *) us_loop_ext((us_loop_t *) this); diff --git a/packages/bun-uws/src/LoopData.h b/packages/bun-uws/src/LoopData.h index e68ca51b0eb0d..96e69eec25e1d 100644 --- a/packages/bun-uws/src/LoopData.h +++ b/packages/bun-uws/src/LoopData.h @@ -63,6 +63,7 @@ struct alignas(16) LoopData { } delete [] corkBuffer; } + void* getCorkedSocket() { return this->corkedSocket; } diff --git a/src/bun.js/bindings/webcore/JSTextEncoder.cpp b/src/bun.js/bindings/webcore/JSTextEncoder.cpp index d886ce16e342e..ec28c91670fd2 100644 --- a/src/bun.js/bindings/webcore/JSTextEncoder.cpp +++ b/src/bun.js/bindings/webcore/JSTextEncoder.cpp @@ -375,10 +375,9 @@ static inline JSC::EncodedJSValue jsTextEncoderPrototypeFunction_encodeBody(JSC: { auto& vm = JSC::getVM(lexicalGlobalObject); auto throwScope = DECLARE_THROW_SCOPE(vm); - UNUSED_PARAM(throwScope); - UNUSED_PARAM(callFrame); EnsureStillAliveScope argument0 = callFrame->argument(0); - JSC::JSString* input = argument0.value().toStringOrNull(lexicalGlobalObject); + JSC::JSString* input = argument0.value().toString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); JSC::EncodedJSValue res; String str; if (input->is8Bit()) { @@ -397,11 +396,6 @@ static inline JSC::EncodedJSValue jsTextEncoderPrototypeFunction_encodeBody(JSC: res = TextEncoder__encode16(lexicalGlobalObject, str.span16().data(), str.length()); } - if (UNLIKELY(JSC::JSValue::decode(res).isObject() && JSC::JSValue::decode(res).getObject()->isErrorInstance())) { - throwScope.throwException(lexicalGlobalObject, JSC::JSValue::decode(res)); - return {}; - } - RELEASE_AND_RETURN(throwScope, res); } diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index aeb9b18d3a82c..29933baf616f9 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -822,7 +822,10 @@ pub const EventLoop = struct { defer this.debug.exit(); if (count == 1) { - this.drainMicrotasksWithGlobal(this.global, this.virtual_machine.jsc); + const vm = this.virtual_machine; + const global = this.global; + const jsc = vm.jsc; + this.drainTasks(vm, global, jsc); } this.entered_event_loop_count -= 1; @@ -1470,6 +1473,19 @@ pub const EventLoop = struct { this.virtual_machine.gc_controller.processGCTimer(); } + pub fn drainTasks(this: *EventLoop, ctx: *JSC.VirtualMachine, global: *JSC.JSGlobalObject, js_vm: *JSC.VM) void { + while (true) { + while (this.tickWithCount(ctx) > 0) : (global.handleRejectedPromises()) { + this.tickConcurrent(); + } else { + this.drainMicrotasksWithGlobal(global, js_vm); + this.tickConcurrent(); + if (this.tasks.count > 0) continue; + } + break; + } + } + pub fn tick(this: *EventLoop) void { JSC.markBinding(@src()); { @@ -1487,16 +1503,7 @@ pub const EventLoop = struct { const global = ctx.global; const global_vm = ctx.jsc; - while (true) { - while (this.tickWithCount(ctx) > 0) : (this.global.handleRejectedPromises()) { - this.tickConcurrent(); - } else { - this.drainMicrotasksWithGlobal(global, global_vm); - this.tickConcurrent(); - if (this.tasks.count > 0) continue; - } - break; - } + this.drainTasks(ctx, global, global_vm); while (this.tickWithCount(ctx) > 0) { this.tickConcurrent(); diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index a7b80833a2866..a84924bf184a0 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -2572,7 +2572,7 @@ pub const VirtualMachine = struct { } pub fn drainMicrotasks(this: *VirtualMachine) void { - this.eventLoop().drainMicrotasks(); + this.eventLoop().drainTasks(this, this.global, this.jsc); } pub fn processFetchLog(globalThis: *JSGlobalObject, specifier: bun.String, referrer: bun.String, log: *logger.Log, ret: *ErrorableResolvedSource, err: anyerror) void { diff --git a/src/bun.js/webcore/encoding.zig b/src/bun.js/webcore/encoding.zig index cd8389eefd4d1..34538d12acd71 100644 --- a/src/bun.js/webcore/encoding.zig +++ b/src/bun.js/webcore/encoding.zig @@ -59,13 +59,14 @@ pub const TextEncoder = struct { const uint8array = JSC.JSValue.createUninitializedUint8Array(globalThis, result.written); bun.assert(result.written <= buf.len); bun.assert(result.read == slice.len); - const array_buffer = uint8array.asArrayBuffer(globalThis).?; + const array_buffer = uint8array.asArrayBuffer(globalThis) orelse return .zero; bun.assert(result.written == array_buffer.len); @memcpy(array_buffer.byteSlice()[0..result.written], buf[0..result.written]); return uint8array; } else { const bytes = strings.allocateLatin1IntoUTF8(globalThis.bunVM().allocator, []const u8, slice) catch { - return JSC.toInvalidArguments("Out of memory", .{}, globalThis); + globalThis.throwOutOfMemory(); + return .zero; }; bun.assert(bytes.len >= slice.len); return ArrayBuffer.fromBytes(bytes, .Uint8Array).toJSUnchecked(globalThis, null); @@ -112,7 +113,8 @@ pub const TextEncoder = struct { @TypeOf(slice), slice, ) catch { - return JSC.toInvalidArguments("Out of memory", .{}, globalThis); + globalThis.throwOutOfMemory(); + return .zero; }; return ArrayBuffer.fromBytes(bytes, .Uint8Array).toJSUnchecked(globalThis, null); } diff --git a/src/deps/libuwsockets.cpp b/src/deps/libuwsockets.cpp index 69da92812deed..1fb7216f3dd2c 100644 --- a/src/deps/libuwsockets.cpp +++ b/src/deps/libuwsockets.cpp @@ -119,6 +119,18 @@ extern "C" } } + extern "C" void uws_res_clear_corked_socket(us_loop_t *loop) { + uWS::LoopData *loopData = uWS::Loop::data(loop); + void *corkedSocket = loopData->getCorkedSocket(); + if (corkedSocket) { + if (loopData->isCorkedSSL()) { + ((uWS::AsyncSocket *) corkedSocket)->uncork(); + } else { + ((uWS::AsyncSocket *) corkedSocket)->uncork(); + } + } +} + void uws_app_delete(int ssl, uws_app_t *app, const char *pattern, uws_method_handler handler, void *user_data) { if (ssl) @@ -1261,9 +1273,19 @@ extern "C" if (ssl) { uWS::HttpResponse *uwsRes = (uWS::HttpResponse *)res; + if (*length < 16 * 1024 && *length > 0) { + if (uwsRes->canCork()) { + uwsRes->uWS::AsyncSocket::cork(); + } + } return uwsRes->write(std::string_view(data, *length), length); } uWS::HttpResponse *uwsRes = (uWS::HttpResponse *)res; + if (*length < 16 * 1024 && *length > 0) { + if (uwsRes->canCork()) { + uwsRes->uWS::AsyncSocket::cork(); + } + } return uwsRes->write(std::string_view(data, *length), length); } uint64_t uws_res_get_write_offset(int ssl, uws_res_r res) nonnull_fn_decl; diff --git a/src/deps/uws.zig b/src/deps/uws.zig index ce6f306064a35..46064329b78dd 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -2377,6 +2377,10 @@ pub const PosixLoop = extern struct { const log = bun.Output.scoped(.Loop, false); + pub fn uncork(this: *PosixLoop) void { + uws_res_clear_corked_socket(this); + } + pub fn iterationNumber(this: *const PosixLoop) u64 { return this.internal_loop_data.iteration_nr; } @@ -4188,6 +4192,10 @@ pub const WindowsLoop = extern struct { pre: *uv.uv_prepare_t, check: *uv.uv_check_t, + pub fn uncork(this: *PosixLoop) void { + uws_res_clear_corked_socket(this); + } + pub fn get() *WindowsLoop { return uws_get_loop_with_native(bun.windows.libuv.Loop.get()); } @@ -4503,3 +4511,4 @@ pub fn onThreadExit() void { } extern fn uws_app_clear_routes(ssl_flag: c_int, app: *uws_app_t) void; +extern fn uws_res_clear_corked_socket(loop: *Loop) void; diff --git a/src/js/node/zlib.ts b/src/js/node/zlib.ts index ef8f56317ba08..9f5bc6020e4b6 100644 --- a/src/js/node/zlib.ts +++ b/src/js/node/zlib.ts @@ -401,22 +401,37 @@ function processChunk(self, chunk, flushFlag, cb) { const handle = self._handle; if (!handle) return process.nextTick(cb); + const outOffset = self._outOffset; + const byteLength = chunk.byteLength; handle.buffer = chunk; handle.cb = cb; - handle.availOutBefore = self._chunkSize - self._outOffset; - handle.availInBefore = chunk.byteLength; + const availOutBefore = self._chunkSize - outOffset; + handle.availInBefore = byteLength; + handle.availOutBefore = availOutBefore; handle.inOff = 0; handle.flushFlag = flushFlag; - - handle.write( - flushFlag, // flush - chunk, // in - 0, // in_off - handle.availInBefore, // in_len - self._outBuffer, // out - self._outOffset, // out_off - handle.availOutBefore, // out_len - ); + if (byteLength < 256) { + handle.writeSync( + flushFlag, // flush + chunk, // in + 0, // in_off + handle.availInBefore, // in_len + self._outBuffer, // out + outOffset, // out_off + availOutBefore, // out_len + ); + processCallback.$call(handle); + } else { + handle.write( + flushFlag, // flush + chunk, // in + 0, // in_off + handle.availInBefore, // in_len + self._outBuffer, // out + outOffset, // out_off + availOutBefore, // out_len + ); + } } function processCallback() { From d5ecd8b5e83d797d6f35419f1c1bb5c0e699efb9 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 15 Oct 2024 17:11:08 -0700 Subject: [PATCH 30/36] Update node_zlib_binding.zig --- src/bun.js/node/node_zlib_binding.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/bun.js/node/node_zlib_binding.zig b/src/bun.js/node/node_zlib_binding.zig index c68598ad37f84..02aca9890f107 100644 --- a/src/bun.js/node/node_zlib_binding.zig +++ b/src/bun.js/node/node_zlib_binding.zig @@ -301,6 +301,10 @@ const CountedKeepAlive = struct { this.keep_alive.unref(vm); } } + + pub fn deinit(this: *@This()) void { + this.keep_alive.disable(); + } }; pub const SNativeZlib = struct { @@ -402,6 +406,7 @@ pub const SNativeZlib = struct { pub fn deinit(this: *@This()) void { this.write_callback.deinit(); this.onerror_value.deinit(); + this.poll_ref.deinit(); this.destroy(); } }; @@ -765,6 +770,7 @@ pub const SNativeBrotli = struct { pub fn deinit(this: *@This()) void { this.write_callback.deinit(); this.onerror_value.deinit(); + this.poll_ref.deinit(); this.destroy(); } }; From b4a1aed1318afdf80af677530591059c5823393e Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 15 Oct 2024 17:58:16 -0700 Subject: [PATCH 31/36] Update zlib.ts --- src/js/node/zlib.ts | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/src/js/node/zlib.ts b/src/js/node/zlib.ts index 9f5bc6020e4b6..aae4c5b26cf7e 100644 --- a/src/js/node/zlib.ts +++ b/src/js/node/zlib.ts @@ -410,28 +410,15 @@ function processChunk(self, chunk, flushFlag, cb) { handle.availOutBefore = availOutBefore; handle.inOff = 0; handle.flushFlag = flushFlag; - if (byteLength < 256) { - handle.writeSync( - flushFlag, // flush - chunk, // in - 0, // in_off - handle.availInBefore, // in_len - self._outBuffer, // out - outOffset, // out_off - availOutBefore, // out_len - ); - processCallback.$call(handle); - } else { - handle.write( - flushFlag, // flush - chunk, // in - 0, // in_off - handle.availInBefore, // in_len - self._outBuffer, // out - outOffset, // out_off - availOutBefore, // out_len - ); - } + handle.write( + flushFlag, // flush + chunk, // in + 0, // in_off + handle.availInBefore, // in_len + self._outBuffer, // out + outOffset, // out_off + availOutBefore, // out_len + ); } function processCallback() { From 748eb45476b389e71a2ef464c2e4a33c9730a3c2 Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Thu, 17 Oct 2024 22:33:23 -0700 Subject: [PATCH 32/36] estimated size --- src/bun.js/api/bun/h2_frame_parser.zig | 108 +++++++++++++++++-------- src/bun.js/api/h2.classes.ts | 9 ++- src/bun.js/bindings/Strong.cpp | 1 + src/bun.js/bindings/Strong.h | 5 ++ src/js/node/http2.ts | 2 + 5 files changed, 89 insertions(+), 36 deletions(-) diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index 535db8e26fa93..fc1d5b8e110f0 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -734,6 +734,8 @@ pub const H2FrameParser = struct { autouncork_registered: bool = false, has_nonnative_backpressure: bool = false, ref_count: u8 = 1, + has_pending_activity: std.atomic.Value(bool) = std.atomic.Value(bool).init(true), + reported_estimated_size: usize = 0, threadlocal var shared_request_buffer: [16384]u8 = undefined; /// The streams hashmap may mutate when growing we use this when we need to make sure its safe to iterate over it @@ -919,6 +921,7 @@ pub const H2FrameParser = struct { .max => return @min(maxLen - frameLen, 255), } } + pub fn flushQueue(this: *Stream, client: *H2FrameParser, written: *usize) FlushState { if (this.canSendData()) { // flush one frame @@ -943,6 +946,7 @@ pub const H2FrameParser = struct { } else { // flush with some payload client.queuedDataSize -= frame.len; + const padding = this.getPadding(frame.len, MAX_PAYLOAD_SIZE_WITHOUT_FRAME - 1); const payload_size = frame.len + (if (padding != 0) padding + 1 else 0); var flags: u8 = if (frame.end_stream and !this.waitForTrailers) @intFromEnum(DataFrameFlags.END_STREAM) else 0; @@ -1051,12 +1055,13 @@ pub const H2FrameParser = struct { }; if (bytes.len > 0) { @memcpy(frame.buffer[0..bytes.len], bytes); - client.globalThis.vm().reportExtraMemory(bytes.len); } log("dataFrame enqueued {}", .{frame.len}); this.dataFrameQueue.enqueue(frame, client.allocator); client.outboundQueueSize += 1; client.queuedDataSize += frame.len; + client.calculateEstimatedByteSize(); + } pub fn init(streamIdentifier: u32, initialWindowSize: u32) Stream { @@ -1132,6 +1137,8 @@ pub const H2FrameParser = struct { frame.deinit(client.allocator); client.outboundQueueSize -= 1; } + client.calculateEstimatedByteSize(); + } /// this can be called multiple times pub fn freeResources(this: *Stream, client: *H2FrameParser, comptime finalizing: bool) void { @@ -1473,6 +1480,7 @@ pub const H2FrameParser = struct { this.writeBuffer.len = MAX_BUFFER_SIZE; this.writeBuffer.shrinkAndFree(this.allocator, MAX_BUFFER_SIZE); this.writeBuffer.clearRetainingCapacity(); + this.calculateEstimatedByteSize(); } log("_genericFlush {}", .{buffer.len}); } else { @@ -1483,6 +1491,8 @@ pub const H2FrameParser = struct { pub fn _genericWrite(this: *H2FrameParser, comptime T: type, socket: T, bytes: []const u8) bool { log("_genericWrite {}", .{bytes.len}); + defer this.calculateEstimatedByteSize(); + const buffer = this.writeBuffer.slice()[this.writeBufferOffset..]; if (buffer.len > 0) { @@ -1494,7 +1504,6 @@ pub const H2FrameParser = struct { // we still have more to buffer and even more now _ = this.writeBuffer.write(this.allocator, bytes) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(bytes.len); log("_genericWrite flushed {} and buffered more {}", .{ written, bytes.len }); return false; @@ -1510,7 +1519,6 @@ pub const H2FrameParser = struct { const pending = bytes[written..]; // ops not all data was sent, lets buffer again _ = this.writeBuffer.write(this.allocator, pending) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(pending.len); log("_genericWrite buffered more {}", .{pending.len}); return false; @@ -1521,6 +1529,7 @@ pub const H2FrameParser = struct { this.writeBuffer.len = MAX_BUFFER_SIZE; this.writeBuffer.shrinkAndFree(this.allocator, MAX_BUFFER_SIZE); this.writeBuffer.clearRetainingCapacity(); + this.calculateEstimatedByteSize(); } return true; } @@ -1530,7 +1539,6 @@ pub const H2FrameParser = struct { const pending = bytes[written..]; // ops not all data was sent, lets buffer again _ = this.writeBuffer.write(this.allocator, pending) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(pending.len); return false; } @@ -1540,6 +1548,8 @@ pub const H2FrameParser = struct { fn flushStreamQueue(this: *H2FrameParser) usize { log("flushStreamQueue {}", .{this.outboundQueueSize}); var written: usize = 0; + defer this.calculateEstimatedByteSize(); + // try to send as much as we can until we reach backpressure while (this.outboundQueueSize > 0) { var it = StreamResumableIterator.init(this); @@ -1575,6 +1585,7 @@ pub const H2FrameParser = struct { this.writeBuffer.len = MAX_BUFFER_SIZE; this.writeBuffer.shrinkAndFree(this.allocator, MAX_BUFFER_SIZE); this.writeBuffer.clearRetainingCapacity(); + this.calculateEstimatedByteSize(); } } const output_value = this.handlers.binary_type.toJS(bytes, this.handlers.globalObject); @@ -1602,10 +1613,11 @@ pub const H2FrameParser = struct { .tls_writeonly, .tls => |socket| this._genericWrite(*TLSSocket, socket, bytes), .tcp_writeonly, .tcp => |socket| this._genericWrite(*TCPSocket, socket, bytes), else => { + defer this.calculateEstimatedByteSize(); + if (this.has_nonnative_backpressure) { // we should not invoke JS when we have backpressure is cheaper to keep it queued here _ = this.writeBuffer.write(this.allocator, bytes) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(bytes.len); return false; } @@ -1617,7 +1629,6 @@ pub const H2FrameParser = struct { -1 => { // dropped _ = this.writeBuffer.write(this.allocator, bytes) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(bytes.len); this.has_nonnative_backpressure = true; }, 0 => { @@ -1648,6 +1659,7 @@ pub const H2FrameParser = struct { if (bytes.len > 0) { _ = this._write(bytes); } + CORKED_H2 = null; } } } @@ -1704,7 +1716,7 @@ pub const H2FrameParser = struct { if (this.remainingLength > 0) { // buffer more data _ = this.readBuffer.appendSlice(payload) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(payload.len); + this.calculateEstimatedByteSize(); return null; } else if (this.remainingLength < 0) { @@ -1717,7 +1729,8 @@ pub const H2FrameParser = struct { if (this.readBuffer.list.items.len > 0) { // return buffered data _ = this.readBuffer.appendSlice(payload) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(payload.len); + this.calculateEstimatedByteSize(); + return .{ .data = this.readBuffer.list.items, @@ -2198,6 +2211,7 @@ pub const H2FrameParser = struct { const settings = this.remoteSettings orelse this.localSettings; const entry = this.streams.getOrPut(streamIdentifier) catch bun.outOfMemory(); entry.value_ptr.* = Stream.init(streamIdentifier, settings.initialWindowSize); + this.calculateEstimatedByteSize(); const ctx_value = this.strong_ctx.get() orelse return entry.value_ptr; const callback = this.handlers.onStreamStart; if (callback != .zero) { @@ -2256,7 +2270,8 @@ pub const H2FrameParser = struct { if (total < FrameHeader.byteSize) { // buffer more data _ = this.readBuffer.appendSlice(bytes) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(bytes.len); + this.calculateEstimatedByteSize(); + return bytes.len; } @@ -2296,7 +2311,7 @@ pub const H2FrameParser = struct { if (bytes.len < FrameHeader.byteSize) { // buffer more dheaderata this.readBuffer.appendSlice(bytes) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(bytes.len); + this.calculateEstimatedByteSize(); return bytes.len; } @@ -3111,6 +3126,27 @@ pub const H2FrameParser = struct { this.dispatchWithExtra(.onStreamEnd, identifier, JSC.JSValue.jsNumber(@intFromEnum(stream.state))); return .undefined; } + + fn getNextStreamID(this: *H2FrameParser) u32 { + var stream_id: u32 = this.lastStreamID; + if (this.isServer) { + if (stream_id % 2 == 0) { + stream_id += 2; + } else { + stream_id += 1; + } + } else { + if (stream_id % 2 == 0) { + stream_id += 1; + } else if (stream_id == 0) { + stream_id = 1; + } else { + stream_id += 2; + } + } + return stream_id; + } + pub fn writeStream(this: *H2FrameParser, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSValue { JSC.markBinding(@src()); const args = callframe.argumentsUndef(5); @@ -3168,26 +3204,6 @@ pub const H2FrameParser = struct { return JSC.JSValue.jsBoolean(true); } - fn getNextStreamID(this: *H2FrameParser) u32 { - var stream_id: u32 = this.lastStreamID; - if (this.isServer) { - if (stream_id % 2 == 0) { - stream_id += 2; - } else { - stream_id += 1; - } - } else { - if (stream_id % 2 == 0) { - stream_id += 1; - } else if (stream_id == 0) { - stream_id = 1; - } else { - stream_id += 2; - } - } - return stream_id; - } - pub fn hasNativeRead(this: *H2FrameParser, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) JSValue { return JSC.JSValue.jsBoolean(this.native_socket == .tcp or this.native_socket == .tls); } @@ -3817,6 +3833,7 @@ pub const H2FrameParser = struct { }); } }; + this.calculateEstimatedByteSize(); // check if socket is provided, and if it is a valid native socket if (options.get(globalObject, "native")) |socket_js| { if (JSTLSSocket.fromJS(socket_js)) |socket| { @@ -3894,14 +3911,17 @@ pub const H2FrameParser = struct { } return this; } - pub fn detachFromJS(this: *H2FrameParser, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) JSValue { + pub fn detachFromJS(this: *H2FrameParser, _: *JSC.JSGlobalObject, _: *JSC.CallFrame, this_value: JSC.JSValue) JSValue { JSC.markBinding(@src()); this.detach(false); + _ = H2FrameParser.dangerouslySetPtr(this_value, null); + this.deref(); return .undefined; } /// be careful when calling detach be sure that the socket is closed and the parser not accesible anymore /// this function can be called multiple times, it will erase stream info pub fn detach(this: *H2FrameParser, comptime finalizing: bool) void { + log("detach {s}", .{ if(this.isServer) "server" else "client" }); this.flushCorked(); this.detachNativeSocket(); this.strong_ctx.deinit(); @@ -3922,8 +3942,12 @@ pub const H2FrameParser = struct { stream.freeResources(this, finalizing); } var streams = this.streams; - defer streams.deinit(); this.streams = bun.U32HashMap(Stream).init(bun.default_allocator); + streams.deinit(); + + this.has_pending_activity.store(false, .release); + this.calculateEstimatedByteSize(); + } pub fn deinit(this: *H2FrameParser) void { @@ -3939,10 +3963,28 @@ pub const H2FrameParser = struct { this.detach(true); } + pub fn estimatedSize(this: ?*H2FrameParser) callconv(.C) usize { + if(this == null) { + return 0; + } + return this.?.reported_estimated_size; + } + + pub fn calculateEstimatedByteSize(this: *H2FrameParser) void { + this.reported_estimated_size = @sizeOf(H2FrameParser) + this.writeBuffer.len + this.queuedDataSize + (this.streams.capacity() * @sizeOf(Stream)); + } + pub fn hasPendingActivity(this: ?*H2FrameParser) callconv(.C) bool { + @fence(.acquire); + if(this == null) { + return false; + } + return this.?.has_pending_activity.load(.acquire); + } + pub fn finalize( this: *H2FrameParser, ) void { - log("finalize", .{}); + log("finalize {s}", .{ if(this.isServer) "server" else "client" }); this.deref(); } }; diff --git a/src/bun.js/api/h2.classes.ts b/src/bun.js/api/h2.classes.ts index bcad57f64a45e..c5f05e3491b8a 100644 --- a/src/bun.js/api/h2.classes.ts +++ b/src/bun.js/api/h2.classes.ts @@ -4,6 +4,11 @@ export default [ define({ name: "H2FrameParser", JSType: "0b11101110", + finalize: true, + construct: true, + hasPendingActivity: true, + estimatedSize: true, + klass: {}, proto: { request: { fn: "request", @@ -40,6 +45,7 @@ export default [ detach: { fn: "detachFromJS", length: 0, + passThis: true, }, rstStream: { fn: "rstStream", @@ -106,8 +112,5 @@ export default [ length: 0, }, }, - finalize: true, - construct: true, - klass: {}, }), ]; diff --git a/src/bun.js/bindings/Strong.cpp b/src/bun.js/bindings/Strong.cpp index 20728656e673e..c5fc34f24077f 100644 --- a/src/bun.js/bindings/Strong.cpp +++ b/src/bun.js/bindings/Strong.cpp @@ -17,6 +17,7 @@ WTF_MAKE_ISO_ALLOCATED_IMPL(StrongRef); extern "C" void Bun__StrongRef__delete(Bun::StrongRef* strongRef) { + strongRef->m_cell.clear(); delete strongRef; } diff --git a/src/bun.js/bindings/Strong.h b/src/bun.js/bindings/Strong.h index f8b2bd7b01179..00f3f58f53fb6 100644 --- a/src/bun.js/bindings/Strong.h +++ b/src/bun.js/bindings/Strong.h @@ -28,6 +28,11 @@ class StrongRef { { } + ~StrongRef() + { + m_cell.clear(); + } + JSC::Strong m_cell; }; diff --git a/src/js/node/http2.ts b/src/js/node/http2.ts index 72936d97851a1..174f65a68d4fb 100644 --- a/src/js/node/http2.ts +++ b/src/js/node/http2.ts @@ -2388,9 +2388,11 @@ class ServerHttp2Session extends Http2Session { this.#parser = null; } this.close(); + this[bunHTTP2Socket] = null; } #onError(error: Error) { + this[bunHTTP2Socket] = null; this.destroy(error); } From 158526601297a02f850159bb9bae86221852f1f4 Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Fri, 18 Oct 2024 17:02:53 +0000 Subject: [PATCH 33/36] `bun run zig-format` --- src/bun.js/api/bun/h2_frame_parser.zig | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index fc1d5b8e110f0..464484a667f3f 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -1061,7 +1061,6 @@ pub const H2FrameParser = struct { client.outboundQueueSize += 1; client.queuedDataSize += frame.len; client.calculateEstimatedByteSize(); - } pub fn init(streamIdentifier: u32, initialWindowSize: u32) Stream { @@ -1138,7 +1137,6 @@ pub const H2FrameParser = struct { client.outboundQueueSize -= 1; } client.calculateEstimatedByteSize(); - } /// this can be called multiple times pub fn freeResources(this: *Stream, client: *H2FrameParser, comptime finalizing: bool) void { @@ -1491,8 +1489,7 @@ pub const H2FrameParser = struct { pub fn _genericWrite(this: *H2FrameParser, comptime T: type, socket: T, bytes: []const u8) bool { log("_genericWrite {}", .{bytes.len}); - defer this.calculateEstimatedByteSize(); - + defer this.calculateEstimatedByteSize(); const buffer = this.writeBuffer.slice()[this.writeBufferOffset..]; if (buffer.len > 0) { @@ -1731,7 +1728,6 @@ pub const H2FrameParser = struct { _ = this.readBuffer.appendSlice(payload) catch bun.outOfMemory(); this.calculateEstimatedByteSize(); - return .{ .data = this.readBuffer.list.items, .end = end, @@ -2272,7 +2268,6 @@ pub const H2FrameParser = struct { _ = this.readBuffer.appendSlice(bytes) catch bun.outOfMemory(); this.calculateEstimatedByteSize(); - return bytes.len; } FrameHeader.from(&header, this.readBuffer.list.items[0..buffered_data], 0, false); @@ -3146,7 +3141,7 @@ pub const H2FrameParser = struct { } return stream_id; } - + pub fn writeStream(this: *H2FrameParser, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSValue { JSC.markBinding(@src()); const args = callframe.argumentsUndef(5); @@ -3921,7 +3916,7 @@ pub const H2FrameParser = struct { /// be careful when calling detach be sure that the socket is closed and the parser not accesible anymore /// this function can be called multiple times, it will erase stream info pub fn detach(this: *H2FrameParser, comptime finalizing: bool) void { - log("detach {s}", .{ if(this.isServer) "server" else "client" }); + log("detach {s}", .{if (this.isServer) "server" else "client"}); this.flushCorked(); this.detachNativeSocket(); this.strong_ctx.deinit(); @@ -3944,10 +3939,9 @@ pub const H2FrameParser = struct { var streams = this.streams; this.streams = bun.U32HashMap(Stream).init(bun.default_allocator); streams.deinit(); - + this.has_pending_activity.store(false, .release); this.calculateEstimatedByteSize(); - } pub fn deinit(this: *H2FrameParser) void { @@ -3964,7 +3958,7 @@ pub const H2FrameParser = struct { } pub fn estimatedSize(this: ?*H2FrameParser) callconv(.C) usize { - if(this == null) { + if (this == null) { return 0; } return this.?.reported_estimated_size; @@ -3972,10 +3966,10 @@ pub const H2FrameParser = struct { pub fn calculateEstimatedByteSize(this: *H2FrameParser) void { this.reported_estimated_size = @sizeOf(H2FrameParser) + this.writeBuffer.len + this.queuedDataSize + (this.streams.capacity() * @sizeOf(Stream)); - } + } pub fn hasPendingActivity(this: ?*H2FrameParser) callconv(.C) bool { @fence(.acquire); - if(this == null) { + if (this == null) { return false; } return this.?.has_pending_activity.load(.acquire); @@ -3984,7 +3978,7 @@ pub const H2FrameParser = struct { pub fn finalize( this: *H2FrameParser, ) void { - log("finalize {s}", .{ if(this.isServer) "server" else "client" }); + log("finalize {s}", .{if (this.isServer) "server" else "client"}); this.deref(); } }; From 5e3276c6c53b719f496a1b36cbf0aa5677b6d276 Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Fri, 18 Oct 2024 10:25:03 -0700 Subject: [PATCH 34/36] nop --- src/bun.js/api/bun/h2_frame_parser.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index 464484a667f3f..0c08fd7c6b235 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -3909,8 +3909,6 @@ pub const H2FrameParser = struct { pub fn detachFromJS(this: *H2FrameParser, _: *JSC.JSGlobalObject, _: *JSC.CallFrame, this_value: JSC.JSValue) JSValue { JSC.markBinding(@src()); this.detach(false); - _ = H2FrameParser.dangerouslySetPtr(this_value, null); - this.deref(); return .undefined; } /// be careful when calling detach be sure that the socket is closed and the parser not accesible anymore From 1e127cf5ff9b47f1a1a3206be56b1e7116726cc5 Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Fri, 18 Oct 2024 10:35:42 -0700 Subject: [PATCH 35/36] not used --- src/bun.js/api/bun/h2_frame_parser.zig | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index 0c08fd7c6b235..bc56933769f14 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -1061,6 +1061,7 @@ pub const H2FrameParser = struct { client.outboundQueueSize += 1; client.queuedDataSize += frame.len; client.calculateEstimatedByteSize(); + } pub fn init(streamIdentifier: u32, initialWindowSize: u32) Stream { @@ -1137,6 +1138,7 @@ pub const H2FrameParser = struct { client.outboundQueueSize -= 1; } client.calculateEstimatedByteSize(); + } /// this can be called multiple times pub fn freeResources(this: *Stream, client: *H2FrameParser, comptime finalizing: bool) void { @@ -1489,7 +1491,8 @@ pub const H2FrameParser = struct { pub fn _genericWrite(this: *H2FrameParser, comptime T: type, socket: T, bytes: []const u8) bool { log("_genericWrite {}", .{bytes.len}); - defer this.calculateEstimatedByteSize(); + defer this.calculateEstimatedByteSize(); + const buffer = this.writeBuffer.slice()[this.writeBufferOffset..]; if (buffer.len > 0) { @@ -1728,6 +1731,7 @@ pub const H2FrameParser = struct { _ = this.readBuffer.appendSlice(payload) catch bun.outOfMemory(); this.calculateEstimatedByteSize(); + return .{ .data = this.readBuffer.list.items, .end = end, @@ -2268,6 +2272,7 @@ pub const H2FrameParser = struct { _ = this.readBuffer.appendSlice(bytes) catch bun.outOfMemory(); this.calculateEstimatedByteSize(); + return bytes.len; } FrameHeader.from(&header, this.readBuffer.list.items[0..buffered_data], 0, false); @@ -3141,7 +3146,7 @@ pub const H2FrameParser = struct { } return stream_id; } - + pub fn writeStream(this: *H2FrameParser, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSValue { JSC.markBinding(@src()); const args = callframe.argumentsUndef(5); @@ -3906,7 +3911,7 @@ pub const H2FrameParser = struct { } return this; } - pub fn detachFromJS(this: *H2FrameParser, _: *JSC.JSGlobalObject, _: *JSC.CallFrame, this_value: JSC.JSValue) JSValue { + pub fn detachFromJS(this: *H2FrameParser, _: *JSC.JSGlobalObject, _: *JSC.CallFrame, _: JSC.JSValue) JSValue { JSC.markBinding(@src()); this.detach(false); return .undefined; @@ -3914,7 +3919,7 @@ pub const H2FrameParser = struct { /// be careful when calling detach be sure that the socket is closed and the parser not accesible anymore /// this function can be called multiple times, it will erase stream info pub fn detach(this: *H2FrameParser, comptime finalizing: bool) void { - log("detach {s}", .{if (this.isServer) "server" else "client"}); + log("detach {s}", .{ if(this.isServer) "server" else "client" }); this.flushCorked(); this.detachNativeSocket(); this.strong_ctx.deinit(); @@ -3937,9 +3942,10 @@ pub const H2FrameParser = struct { var streams = this.streams; this.streams = bun.U32HashMap(Stream).init(bun.default_allocator); streams.deinit(); - + this.has_pending_activity.store(false, .release); this.calculateEstimatedByteSize(); + } pub fn deinit(this: *H2FrameParser) void { @@ -3956,7 +3962,7 @@ pub const H2FrameParser = struct { } pub fn estimatedSize(this: ?*H2FrameParser) callconv(.C) usize { - if (this == null) { + if(this == null) { return 0; } return this.?.reported_estimated_size; @@ -3964,10 +3970,10 @@ pub const H2FrameParser = struct { pub fn calculateEstimatedByteSize(this: *H2FrameParser) void { this.reported_estimated_size = @sizeOf(H2FrameParser) + this.writeBuffer.len + this.queuedDataSize + (this.streams.capacity() * @sizeOf(Stream)); - } + } pub fn hasPendingActivity(this: ?*H2FrameParser) callconv(.C) bool { @fence(.acquire); - if (this == null) { + if(this == null) { return false; } return this.?.has_pending_activity.load(.acquire); @@ -3976,7 +3982,7 @@ pub const H2FrameParser = struct { pub fn finalize( this: *H2FrameParser, ) void { - log("finalize {s}", .{if (this.isServer) "server" else "client"}); + log("finalize {s}", .{ if(this.isServer) "server" else "client" }); this.deref(); } }; From 61ccc78fe4d73c87a868a292488b2014f79c253d Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Fri, 18 Oct 2024 17:38:48 +0000 Subject: [PATCH 36/36] `bun run zig-format` --- src/bun.js/api/bun/h2_frame_parser.zig | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index bc56933769f14..14a9a7d094f2f 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -1061,7 +1061,6 @@ pub const H2FrameParser = struct { client.outboundQueueSize += 1; client.queuedDataSize += frame.len; client.calculateEstimatedByteSize(); - } pub fn init(streamIdentifier: u32, initialWindowSize: u32) Stream { @@ -1138,7 +1137,6 @@ pub const H2FrameParser = struct { client.outboundQueueSize -= 1; } client.calculateEstimatedByteSize(); - } /// this can be called multiple times pub fn freeResources(this: *Stream, client: *H2FrameParser, comptime finalizing: bool) void { @@ -1491,8 +1489,7 @@ pub const H2FrameParser = struct { pub fn _genericWrite(this: *H2FrameParser, comptime T: type, socket: T, bytes: []const u8) bool { log("_genericWrite {}", .{bytes.len}); - defer this.calculateEstimatedByteSize(); - + defer this.calculateEstimatedByteSize(); const buffer = this.writeBuffer.slice()[this.writeBufferOffset..]; if (buffer.len > 0) { @@ -1731,7 +1728,6 @@ pub const H2FrameParser = struct { _ = this.readBuffer.appendSlice(payload) catch bun.outOfMemory(); this.calculateEstimatedByteSize(); - return .{ .data = this.readBuffer.list.items, .end = end, @@ -2272,7 +2268,6 @@ pub const H2FrameParser = struct { _ = this.readBuffer.appendSlice(bytes) catch bun.outOfMemory(); this.calculateEstimatedByteSize(); - return bytes.len; } FrameHeader.from(&header, this.readBuffer.list.items[0..buffered_data], 0, false); @@ -3146,7 +3141,7 @@ pub const H2FrameParser = struct { } return stream_id; } - + pub fn writeStream(this: *H2FrameParser, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSValue { JSC.markBinding(@src()); const args = callframe.argumentsUndef(5); @@ -3919,7 +3914,7 @@ pub const H2FrameParser = struct { /// be careful when calling detach be sure that the socket is closed and the parser not accesible anymore /// this function can be called multiple times, it will erase stream info pub fn detach(this: *H2FrameParser, comptime finalizing: bool) void { - log("detach {s}", .{ if(this.isServer) "server" else "client" }); + log("detach {s}", .{if (this.isServer) "server" else "client"}); this.flushCorked(); this.detachNativeSocket(); this.strong_ctx.deinit(); @@ -3942,10 +3937,9 @@ pub const H2FrameParser = struct { var streams = this.streams; this.streams = bun.U32HashMap(Stream).init(bun.default_allocator); streams.deinit(); - + this.has_pending_activity.store(false, .release); this.calculateEstimatedByteSize(); - } pub fn deinit(this: *H2FrameParser) void { @@ -3962,7 +3956,7 @@ pub const H2FrameParser = struct { } pub fn estimatedSize(this: ?*H2FrameParser) callconv(.C) usize { - if(this == null) { + if (this == null) { return 0; } return this.?.reported_estimated_size; @@ -3970,10 +3964,10 @@ pub const H2FrameParser = struct { pub fn calculateEstimatedByteSize(this: *H2FrameParser) void { this.reported_estimated_size = @sizeOf(H2FrameParser) + this.writeBuffer.len + this.queuedDataSize + (this.streams.capacity() * @sizeOf(Stream)); - } + } pub fn hasPendingActivity(this: ?*H2FrameParser) callconv(.C) bool { @fence(.acquire); - if(this == null) { + if (this == null) { return false; } return this.?.has_pending_activity.load(.acquire); @@ -3982,7 +3976,7 @@ pub const H2FrameParser = struct { pub fn finalize( this: *H2FrameParser, ) void { - log("finalize {s}", .{ if(this.isServer) "server" else "client" }); + log("finalize {s}", .{if (this.isServer) "server" else "client"}); this.deref(); } };