diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index 5f99735d252752..ab7c421e48bbfa 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -3353,14 +3353,16 @@ pub const H2FrameParser = struct { if (this.isServer) { if (!ValidPseudoHeaders.has(name)) { - const exception = JSC.toTypeError(.ERR_HTTP2_INVALID_PSEUDOHEADER, "\"{s}\" is an invalid pseudoheader or is used incorrectly", .{name}, globalObject); - globalObject.throwValue(exception); + if (!globalObject.hasException()) { + globalObject.ERR_HTTP2_INVALID_PSEUDOHEADER("\"{s}\" is an invalid pseudoheader or is used incorrectly", .{name}).throw(); + } return .zero; } } else { if (!ValidRequestPseudoHeaders.has(name)) { - const exception = JSC.toTypeError(.ERR_HTTP2_INVALID_PSEUDOHEADER, "\"{s}\" is an invalid pseudoheader or is used incorrectly", .{name}, globalObject); - globalObject.throwValue(exception); + if (!globalObject.hasException()) { + globalObject.ERR_HTTP2_INVALID_PSEUDOHEADER("\"{s}\" is an invalid pseudoheader or is used incorrectly", .{name}).throw(); + } return .zero; } } @@ -3368,9 +3370,10 @@ pub const H2FrameParser = struct { continue; } - var js_value = try headers_arg.getTruthy(globalObject, name) orelse { - const exception = JSC.toTypeError(.ERR_HTTP2_INVALID_HEADER_VALUE, "Invalid value for header \"{s}\"", .{name}, globalObject); - globalObject.throwValue(exception); + const js_value: JSC.JSValue = try headers_arg.get(globalObject, name) orelse { + if (!globalObject.hasException()) { + globalObject.ERR_HTTP2_INVALID_HEADER_VALUE("Invalid value for header \"{s}\"", .{name}).throw(); + } return .zero; }; @@ -3380,21 +3383,24 @@ pub const H2FrameParser = struct { var value_iter = js_value.arrayIterator(globalObject); if (SingleValueHeaders.has(name) and value_iter.len > 1) { - const exception = JSC.toTypeError(.ERR_HTTP2_INVALID_HEADER_VALUE, "Header field \"{s}\" must only have a single value", .{name}, globalObject); - globalObject.throwValue(exception); + if (!globalObject.hasException()) { + globalObject.ERR_HTTP2_INVALID_HEADER_VALUE("Header field \"{s}\" must only have a single value", .{name}).throw(); + } return .zero; } while (value_iter.next()) |item| { if (item.isEmptyOrUndefinedOrNull()) { - const exception = JSC.toTypeError(.ERR_HTTP2_INVALID_HEADER_VALUE, "Invalid value for header \"{s}\"", .{name}, globalObject); - globalObject.throwValue(exception); + if (!globalObject.hasException()) { + globalObject.ERR_HTTP2_INVALID_HEADER_VALUE("Invalid value for header \"{s}\"", .{name}).throw(); + } return .zero; } const value_str = item.toStringOrNull(globalObject) orelse { - const exception = JSC.toTypeError(.ERR_HTTP2_INVALID_HEADER_VALUE, "Invalid value for header \"{s}\"", .{name}, globalObject); - globalObject.throwValue(exception); + if (!globalObject.hasException()) { + globalObject.ERR_HTTP2_INVALID_HEADER_VALUE("Invalid value for header \"{s}\"", .{name}).throw(); + } return .zero; }; @@ -3417,11 +3423,12 @@ pub const H2FrameParser = struct { return .undefined; }; } - } else { + } else if (!js_value.isEmptyOrUndefinedOrNull()) { log("single header {s}", .{name}); const value_str = js_value.toStringOrNull(globalObject) orelse { - const exception = JSC.toTypeError(.ERR_HTTP2_INVALID_HEADER_VALUE, "Invalid value for header \"{s}\"", .{name}, globalObject); - globalObject.throwValue(exception); + if (!globalObject.hasException()) { + globalObject.ERR_HTTP2_INVALID_HEADER_VALUE("Invalid value for header \"{s}\"", .{name}).throw(); + } return .zero; }; diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 5aeb892712623e..861d5d002bfe3e 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -313,6 +313,7 @@ pub const SocketConfig = struct { pub fn fromJS(vm: *JSC.VirtualMachine, opts: JSC.JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!SocketConfig { var hostname_or_unix: JSC.ZigString.Slice = JSC.ZigString.Slice.empty; + errdefer hostname_or_unix.deinit(); var port: ?u16 = null; var exclusive = false; var allowHalfOpen = false; @@ -332,6 +333,12 @@ pub const SocketConfig = struct { } } + errdefer { + if (ssl != null) { + ssl.?.deinit(); + } + } + hostname_or_unix: { if (try opts.getTruthy(globalObject, "fd")) |fd_| { if (fd_.isNumber()) { @@ -339,16 +346,18 @@ pub const SocketConfig = struct { } } - if (try opts.getTruthy(globalObject, "unix")) |unix_socket| { - if (!unix_socket.isString()) { - return globalObject.throwInvalidArguments("Expected \"unix\" to be a string", .{}); - } + if (try opts.getStringish(globalObject, "unix")) |unix_socket| { + defer unix_socket.deref(); - hostname_or_unix = unix_socket.getZigString(globalObject).toSlice(bun.default_allocator); + hostname_or_unix = try unix_socket.toUTF8WithoutRef(bun.default_allocator).cloneIfNeeded(bun.default_allocator); if (strings.hasPrefixComptime(hostname_or_unix.slice(), "file://") or strings.hasPrefixComptime(hostname_or_unix.slice(), "unix://") or strings.hasPrefixComptime(hostname_or_unix.slice(), "sock://")) { - hostname_or_unix.ptr += 7; - hostname_or_unix.len -|= 7; + // The memory allocator relies on the pointer address to + // free it, so if we simply moved the pointer up it would + // cause an issue when freeing it later. + const moved_bytes = try bun.default_allocator.dupeZ(u8, hostname_or_unix.slice()[7..]); + hostname_or_unix.deinit(); + hostname_or_unix = ZigString.Slice.init(bun.default_allocator, moved_bytes); } if (hostname_or_unix.len > 0) { @@ -363,20 +372,21 @@ pub const SocketConfig = struct { allowHalfOpen = true; } - if (try opts.getTruthy(globalObject, "hostname") orelse try opts.getTruthy(globalObject, "host")) |hostname| { - if (!hostname.isString()) { - return globalObject.throwInvalidArguments("Expected \"hostname\" to be a string", .{}); - } + if (try opts.getStringish(globalObject, "hostname") orelse try opts.getStringish(globalObject, "host")) |hostname| { + defer hostname.deref(); var port_value = try opts.get(globalObject, "port") orelse JSValue.zero; - hostname_or_unix = hostname.getZigString(globalObject).toSlice(bun.default_allocator); + hostname_or_unix = try hostname.toUTF8WithoutRef(bun.default_allocator).cloneIfNeeded(bun.default_allocator); if (port_value.isEmptyOrUndefinedOrNull() and hostname_or_unix.len > 0) { const parsed_url = bun.URL.parse(hostname_or_unix.slice()); if (parsed_url.getPort()) |port_num| { port_value = JSValue.jsNumber(port_num); - hostname_or_unix.ptr = parsed_url.hostname.ptr; - hostname_or_unix.len = @as(u32, @truncate(parsed_url.hostname.len)); + if (parsed_url.hostname.len > 0) { + const moved_bytes = try bun.default_allocator.dupeZ(u8, parsed_url.hostname); + hostname_or_unix.deinit(); + hostname_or_unix = ZigString.Slice.init(bun.default_allocator, moved_bytes); + } } } @@ -410,7 +420,6 @@ pub const SocketConfig = struct { return globalObject.throwInvalidArguments("Expected either \"hostname\" or \"unix\"", .{}); } - errdefer hostname_or_unix.deinit(); var handlers = try Handlers.fromJS(globalObject, try opts.get(globalObject, "socket") orelse JSValue.zero); diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 87ab2838a3ea0c..4648fafbc9b45d 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -1304,11 +1304,9 @@ pub const ServerConfig = struct { } if (global.hasException()) return error.JSError; - if (try arg.getTruthy(global, "hostname") orelse try arg.getTruthy(global, "host")) |host| { - const host_str = host.toSlice( - global, - bun.default_allocator, - ); + if (try arg.getStringish(global, "hostname") orelse try arg.getStringish(global, "host")) |host| { + defer host.deref(); + const host_str = host.toUTF8(bun.default_allocator); defer host_str.deinit(); if (host_str.len > 0) { @@ -1318,11 +1316,9 @@ pub const ServerConfig = struct { } if (global.hasException()) return error.JSError; - if (try arg.getTruthy(global, "unix")) |unix| { - const unix_str = unix.toSlice( - global, - bun.default_allocator, - ); + if (try arg.getStringish(global, "unix")) |unix| { + defer unix.deref(); + const unix_str = unix.toUTF8(bun.default_allocator); defer unix_str.deinit(); if (unix_str.len > 0) { if (has_hostname) { diff --git a/src/bun.js/bindings/ExposeNodeModuleGlobals.cpp b/src/bun.js/bindings/ExposeNodeModuleGlobals.cpp index a99b6624881755..0497c1c6a6d360 100644 --- a/src/bun.js/bindings/ExposeNodeModuleGlobals.cpp +++ b/src/bun.js/bindings/ExposeNodeModuleGlobals.cpp @@ -123,4 +123,4 @@ extern "C" void Bun__ExposeNodeModuleGlobals(Zig::GlobalObject* globalObject) 0 | JSC::PropertyAttribute::CustomValue ); } -} \ No newline at end of file +} diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index f80dd361480077..b813c614cfec43 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3793,8 +3793,9 @@ JSC__JSValue JSC__JSValue__getIfPropertyExistsImpl(JSC__JSValue JSValue0, JSC::VM& vm = globalObject->vm(); JSC::JSObject* object = value.getObject(); - if (UNLIKELY(!object)) - return JSValue::encode({}); + if (UNLIKELY(!object)) { + return JSValue::encode(JSValue::decode(JSC::JSValue::ValueDeleted)); + } // Since Identifier might not ref' the string, we need to ensure it doesn't get deref'd until this function returns const auto propertyString = String(StringImpl::createWithoutCopying({ arg1, arg2 })); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 63b462787b494f..cfeaee4b8983c0 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -3084,13 +3084,19 @@ pub const JSGlobalObject = opaque { ); if (possible_errors.OutOfMemory and err == error.OutOfMemory) { - bun.assert(!global.hasException()); // dual exception - global.throwOutOfMemory(); + if (global.hasException()) { + if (comptime bun.Environment.isDebug) bun.Output.panic("attempted to throw OutOfMemory without an exception", .{}); + } else { + global.throwOutOfMemory(); + } return null_value; } if (possible_errors.JSError and err == error.JSError) { - bun.assert(global.hasException()); // Exception was cleared, yet returned. + if (!global.hasException()) { + if (comptime bun.Environment.isDebug) bun.Output.panic("attempted to throw JSError without an exception", .{}); + global.throwOutOfMemory(); + } return null_value; } @@ -3736,11 +3742,19 @@ pub const JSValueReprInt = i64; /// ABI-compatible with EncodedJSValue /// In the future, this type will exclude `zero`, encoding it as `error.JSError` instead. pub const JSValue = enum(i64) { - zero = 0, undefined = 0xa, null = 0x2, true = FFI.TrueI64, false = 0x6, + + /// Typically means an exception was thrown. + zero = 0, + + /// JSValue::ValueDeleted + /// + /// Deleted is a special encoding used in JSC hash map internals used for + /// the null state. It is re-used here for encoding the "not present" state. + property_does_not_exist_on_object = 0x4, _, /// When JavaScriptCore throws something, it returns a null cell (0). The @@ -5270,17 +5284,25 @@ pub const JSValue = enum(i64) { // `this` must be known to be an object // intended to be more lightweight than ZigString. pub fn fastGet(this: JSValue, global: *JSGlobalObject, builtin_name: BuiltinName) ?JSValue { - if (bun.Environment.allow_assert) + if (bun.Environment.isDebug) bun.assert(this.isObject()); - const result = JSC__JSValue__fastGet(this, global, @intFromEnum(builtin_name)).legacyUnwrap(); - if (result == .zero or - // JS APIs treat {}.a as mostly the same as though it was not defined - result == .undefined) - { - return null; - } - return result; + return switch (JSC__JSValue__fastGet(this, global, @intFromEnum(builtin_name))) { + .zero, .undefined, .property_does_not_exist_on_object => null, + else => |val| val, + }; + } + + pub fn fastGetWithError(this: JSValue, global: *JSGlobalObject, builtin_name: BuiltinName) JSError!?JSValue { + if (bun.Environment.isDebug) + bun.assert(this.isObject()); + + return switch (JSC__JSValue__fastGet(this, global, @intFromEnum(builtin_name))) { + .zero => error.JSError, + .undefined => null, + .property_does_not_exist_on_object => null, + else => |val| val, + }; } pub fn fastGetDirect(this: JSValue, global: *JSGlobalObject, builtin_name: BuiltinName) ?JSValue { @@ -5292,7 +5314,7 @@ pub const JSValue = enum(i64) { return result; } - extern fn JSC__JSValue__fastGet(value: JSValue, global: *JSGlobalObject, builtin_id: u8) GetResult; + extern fn JSC__JSValue__fastGet(value: JSValue, global: *JSGlobalObject, builtin_id: u8) JSValue; extern fn JSC__JSValue__fastGetOwn(value: JSValue, globalObject: *JSGlobalObject, property: BuiltinName) JSValue; pub fn fastGetOwn(this: JSValue, global: *JSGlobalObject, builtin_name: BuiltinName) ?JSValue { const result = JSC__JSValue__fastGetOwn(this, global, builtin_name); @@ -5307,42 +5329,7 @@ pub const JSValue = enum(i64) { return cppFn("fastGetDirect_", .{ this, global, builtin_name }); } - /// Problem: The `get` needs to model !?JSValue - /// - null -> the property does not exist - /// - error -> the get operation threw - /// - any other JSValue -> success. this could be jsNull() or jsUndefined() - /// - /// `.zero` is already used for the error state - /// - /// Deleted is a special encoding used in JSC hash map internals used for - /// the null state. It is re-used here for encoding the "not present" state. - const GetResult = enum(i64) { - thrown_exception = 0, - does_not_exist = 0x4, // JSC::JSValue::ValueDeleted - _, - - fn legacyUnwrap(value: GetResult) ?JSValue { - return switch (value) { - // footgun! caller must check hasException on every `get` or else Bun will crash - .thrown_exception => null, - - .does_not_exist => null, - else => @enumFromInt(@intFromEnum(value)), - }; - } - - fn unwrap(value: GetResult, global: *JSGlobalObject) JSError!?JSValue { - return switch (value) { - .thrown_exception => { - bun.assert(global.hasException()); - return error.JSError; - }, - .does_not_exist => null, - else => @enumFromInt(@intFromEnum(value)), - }; - } - }; - extern fn JSC__JSValue__getIfPropertyExistsImpl(target: JSValue, global: *JSGlobalObject, ptr: [*]const u8, len: u32) GetResult; + extern fn JSC__JSValue__getIfPropertyExistsImpl(target: JSValue, global: *JSGlobalObject, ptr: [*]const u8, len: u32) JSValue; pub fn getIfPropertyExistsFromPath(this: JSValue, global: *JSGlobalObject, path: JSValue) JSValue { return cppFn("getIfPropertyExistsFromPath", .{ this, global, path }); @@ -5391,7 +5378,10 @@ pub const JSValue = enum(i64) { } } - return JSC__JSValue__getIfPropertyExistsImpl(this, global, property.ptr, @intCast(property.len)).legacyUnwrap(); + return switch (JSC__JSValue__getIfPropertyExistsImpl(this, global, property.ptr, @intCast(property.len))) { + .undefined, .zero, .property_does_not_exist_on_object => null, + else => |val| val, + }; } /// Equivalent to `target[property]`. Calls userland getters/proxies. Can @@ -5403,17 +5393,21 @@ pub const JSValue = enum(i64) { /// marked `inline` to allow Zig to determine if `fastGet` should be used /// per invocation. pub inline fn get(target: JSValue, global: *JSGlobalObject, property: anytype) JSError!?JSValue { - if (bun.Environment.allow_assert) bun.assert(target.isObject()); + if (bun.Environment.isDebug) bun.assert(target.isObject()); const property_slice: []const u8 = property; // must be a slice! // This call requires `get2` to be `inline` if (bun.isComptimeKnown(property_slice)) { - if (comptime BuiltinName.get(property_slice)) |builtin| { - return target.fastGet(global, builtin); + if (comptime BuiltinName.get(property_slice)) |builtin_name| { + return target.fastGetWithError(global, builtin_name); } } - return JSC__JSValue__getIfPropertyExistsImpl(target, global, property_slice.ptr, @intCast(property_slice.len)).unwrap(global); + return switch (JSC__JSValue__getIfPropertyExistsImpl(target, global, property_slice.ptr, @intCast(property_slice.len))) { + .zero => error.JSError, + .undefined, .property_does_not_exist_on_object => null, + else => |val| val, + }; } extern fn JSC__JSValue__getOwn(value: JSValue, globalObject: *JSGlobalObject, propertyName: *const bun.String) JSValue; @@ -5459,15 +5453,33 @@ pub const JSValue = enum(i64) { return getOwnTruthy(this, global, property); } - // TODO: replace calls to this function with `getOptional` - pub fn getTruthyComptime(this: JSValue, global: *JSGlobalObject, comptime property: []const u8) bun.JSError!?JSValue { - if (comptime bun.ComptimeEnumMap(BuiltinName).has(property)) { - if (fastGet(this, global, @field(BuiltinName, property))) |prop| { - if (prop.isEmptyOrUndefinedOrNull()) return null; + pub fn truthyPropertyValue(prop: JSValue) ?JSValue { + return switch (prop) { + .null => null, + + // Handled by get() and fastGet(). + .zero, .undefined => unreachable, + + // false, 0, are deliberately not included in this list. + // That would prevent you from passing `0` or `false` to various Bun APIs. + + else => { + // Ignore empty string. + if (prop.isString()) { + if (!prop.toBoolean()) { + return null; + } + } + return prop; - } + }, + }; + } - return null; + // TODO: replace calls to this function with `getOptional` + pub fn getTruthyComptime(this: JSValue, global: *JSGlobalObject, comptime property: []const u8) bun.JSError!?JSValue { + if (comptime BuiltinName.has(property)) { + return truthyPropertyValue(fastGet(this, global, @field(BuiltinName, property)) orelse return null); } return getTruthy(this, global, property); @@ -5476,13 +5488,43 @@ pub const JSValue = enum(i64) { // TODO: replace calls to this function with `getOptional` pub fn getTruthy(this: JSValue, global: *JSGlobalObject, property: []const u8) bun.JSError!?JSValue { if (try get(this, global, property)) |prop| { - if (prop.isEmptyOrUndefinedOrNull()) return null; - return prop; + return truthyPropertyValue(prop); } return null; } + /// Get a value that can be coerced to a string. + /// + /// Returns null when the value is: + /// - JSValue.null + /// - JSValue.false + /// - JSValue.undefined + /// - an empty string + pub fn getStringish(this: JSValue, global: *JSGlobalObject, property: []const u8) bun.JSError!?bun.String { + const prop = try get(this, global, property) orelse return null; + if (prop.isNull() or prop == .false) { + return null; + } + + if (prop.isSymbol()) { + _ = global.throwInvalidPropertyTypeValue(property, "string", prop); + return error.JSError; + } + + const str = prop.toBunString(global); + if (global.hasException()) { + str.deref(); + return error.JSError; + } + + if (str.isEmpty()) { + return null; + } + + return str; + } + pub fn toEnumFromMap( this: JSValue, globalThis: *JSGlobalObject, diff --git a/src/crash_handler.zig b/src/crash_handler.zig index d9cb08f9899240..c0ea3aafaf60cf 100644 --- a/src/crash_handler.zig +++ b/src/crash_handler.zig @@ -1449,66 +1449,73 @@ fn crash() noreturn { pub var verbose_error_trace = false; -fn handleErrorReturnTraceExtra(err: anyerror, maybe_trace: ?*std.builtin.StackTrace, comptime is_root: bool) void { - if (!builtin.have_error_return_tracing) return; - if (!verbose_error_trace and !is_root) return; - - if (maybe_trace) |trace| { - // The format of the panic trace is slightly different in debug - // builds Mainly, we demangle the backtrace immediately instead - // of using a trace string. - // - // To make the release-mode behavior easier to demo, debug mode - // checks for this CLI flag. - const is_debug = bun.Environment.isDebug and check_flag: { - for (bun.argv) |arg| { - if (bun.strings.eqlComptime(arg, "--debug-crash-handler-use-trace-string")) { - break :check_flag false; - } +noinline fn coldHandleErrorReturnTrace(err_int_workaround_for_zig_ccall_bug: std.meta.Int(.unsigned, @bitSizeOf(anyerror)), trace: *std.builtin.StackTrace, comptime is_root: bool) void { + @setCold(true); + const err = @errorFromInt(err_int_workaround_for_zig_ccall_bug); + + // The format of the panic trace is slightly different in debug + // builds Mainly, we demangle the backtrace immediately instead + // of using a trace string. + // + // To make the release-mode behavior easier to demo, debug mode + // checks for this CLI flag. + const is_debug = bun.Environment.isDebug and check_flag: { + for (bun.argv) |arg| { + if (bun.strings.eqlComptime(arg, "--debug-crash-handler-use-trace-string")) { + break :check_flag false; } - break :check_flag true; - }; + } + break :check_flag true; + }; - if (is_debug) { - if (is_root) { - if (verbose_error_trace) { - Output.note("Release build will not have this trace by default:", .{}); - } - } else { - Output.note( - "caught error.{s}:", - .{@errorName(err)}, - ); + if (is_debug) { + if (is_root) { + if (verbose_error_trace) { + Output.note("Release build will not have this trace by default:", .{}); } - Output.flush(); - dumpStackTrace(trace.*); } else { - const ts = TraceString{ - .trace = trace, - .reason = .{ .zig_error = err }, - .action = .view_trace, - }; - if (is_root) { - Output.prettyErrorln( - \\ - \\To send a redacted crash report to Bun's team, - \\please file a GitHub issue using the link below: - \\ - \\ {} - \\ - , - .{ts}, - ); - } else { - Output.prettyErrorln( - "trace: error.{s}: {}", - .{ @errorName(err), ts }, - ); - } + Output.note( + "caught error.{s}:", + .{@errorName(err)}, + ); + } + Output.flush(); + dumpStackTrace(trace.*); + } else { + const ts = TraceString{ + .trace = trace, + .reason = .{ .zig_error = err }, + .action = .view_trace, + }; + if (is_root) { + Output.prettyErrorln( + \\ + \\To send a redacted crash report to Bun's team, + \\please file a GitHub issue using the link below: + \\ + \\ {} + \\ + , + .{ts}, + ); + } else { + Output.prettyErrorln( + "trace: error.{s}: {}", + .{ @errorName(err), ts }, + ); } } } +inline fn handleErrorReturnTraceExtra(err: anyerror, maybe_trace: ?*std.builtin.StackTrace, comptime is_root: bool) void { + if (!builtin.have_error_return_tracing) return; + if (!verbose_error_trace and !is_root) return; + + if (maybe_trace) |trace| { + coldHandleErrorReturnTrace(@intFromError(err), trace, is_root); + } +} + /// In many places we catch errors, the trace for them is absorbed and only a /// single line (the error name) is printed. When this is set, we will print /// trace strings for those errors (or full stacks in debug builds). diff --git a/src/string.zig b/src/string.zig index d47cc49cf0a9ef..3b2dab9fcb3c15 100644 --- a/src/string.zig +++ b/src/string.zig @@ -706,10 +706,14 @@ pub const String = extern struct { pub fn fromJS2(value: bun.JSC.JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!String { var out: String = String.dead; if (BunString__fromJS(globalObject, value, &out)) { - bun.assert(out.tag != .Dead); + if (comptime bun.Environment.isDebug) { + bun.assert(out.tag != .Dead); + } return out; } else { - bun.assert(globalObject.hasException()); + if (comptime bun.Environment.isDebug) { + bun.assert(globalObject.hasException()); + } return error.JSError; } } @@ -721,7 +725,9 @@ pub const String = extern struct { if (BunString__fromJSRef(globalObject, value, &out)) { return out; } else { - bun.assert(globalObject.hasException()); + if (comptime bun.Environment.isDebug) { + bun.assert(globalObject.hasException()); + } return error.JSError; } } diff --git a/test/harness.ts b/test/harness.ts index 82d3231b6fc914..15a3e8da031d8d 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -1383,3 +1383,21 @@ export function libcPathForDlopen() { throw new Error("TODO"); } } + +export function cwdScope(cwd: string) { + const original = process.cwd(); + process.chdir(cwd); + return { + [Symbol.dispose]() { + process.chdir(original); + }, + }; +} + +export function rmScope(path: string) { + return { + [Symbol.dispose]() { + fs.rmSync(path, { recursive: true, force: true }); + }, + }; +} diff --git a/test/js/bun/http/bun-listen-connect-args.test.ts b/test/js/bun/http/bun-listen-connect-args.test.ts new file mode 100644 index 00000000000000..21e62f2d2c1904 --- /dev/null +++ b/test/js/bun/http/bun-listen-connect-args.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, test } from "bun:test"; +import { cwdScope, isWindows, rmScope, tempDirWithFiles } from "harness"; + +describe.if(!isWindows)("unix socket", () => { + test("valid", () => { + using server = Bun.listen({ + unix: Math.random().toString(32).slice(2, 15) + ".sock", + socket: { + open() {}, + close() {}, + data() {}, + drain() {}, + }, + }); + server.stop(); + }); + + describe("allows", () => { + const permutations = [ + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + port: 0, + hostname: "", + }, + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: undefined, + }, + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: null, + }, + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: false, + }, + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: Buffer.from(""), + }, + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: Buffer.alloc(0), + }, + { + unix: "unix://" + Math.random().toString(32).slice(2, 15) + ".sock", + hostname: Buffer.alloc(0), + }, + ]; + + for (const args of permutations) { + test(`${JSON.stringify(args)}`, async () => { + const tempdir = tempDirWithFiles("test-socket", { + "foo.txt": "bar", + }); + using cwd = cwdScope(tempdir); + using rm = rmScope(tempdir); + for (let i = 0; i < 100; i++) { + using server = Bun.listen({ + ...args, + unix: args.unix.startsWith("unix://") ? "unix://" + i + args.unix.slice(7) : i + args.unix, + socket: { + open() {}, + close() {}, + data() {}, + drain() {}, + }, + }); + server.stop(); + } + }); + } + }); +}); diff --git a/test/js/bun/http/bun-serve-args.test.ts b/test/js/bun/http/bun-serve-args.test.ts new file mode 100644 index 00000000000000..8414c32d60a6e0 --- /dev/null +++ b/test/js/bun/http/bun-serve-args.test.ts @@ -0,0 +1,654 @@ +import { serve } from "bun"; +import { describe, expect, test } from "bun:test"; +import { tmpdirSync } from "../../../harness"; + +const defaultHostname = "localhost"; + +describe("Bun.serve basic options", () => { + test("minimal valid config", () => { + using server = serve({ + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.port).toBeGreaterThan(0); // Default port + expect(server.hostname).toBe(defaultHostname); + server.stop(); + }); + + test("port as string", () => { + using server = serve({ + port: "0", + fetch() { + return new Response("ok"); + }, + }); + expect(server.port).toBeGreaterThan(0); + server.stop(); + }); +}); + +describe("unix socket", () => { + const permutations = [ + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: "", + }, + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: undefined, + }, + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: null, + }, + { + unix: Buffer.from(Math.random().toString(32).slice(2, 15) + ".sock"), + hostname: null, + }, + { + unix: Buffer.from(Math.random().toString(32).slice(2, 15) + ".sock"), + hostname: Buffer.from(""), + }, + ] as const; + + for (const { unix, hostname } of permutations) { + test(`unix: ${unix} and hostname: ${hostname}`, () => { + using server = serve({ + // @ts-expect-error - Testing invalid combination + unix, + // @ts-expect-error - Testing invalid combination + hostname, + port: 0, + fetch() { + return new Response("ok"); + }, + }); + // @ts-expect-error - Testing invalid property + expect(server.address + "").toBe(unix + ""); + expect(server.port).toBeUndefined(); + expect(server.hostname).toBeUndefined(); + server.stop(); + }); + } +}); + +describe("hostname and port works", () => { + const permutations = [ + { + port: 0, + hostname: defaultHostname, + unix: undefined, + }, + { + port: 0, + hostname: undefined, + unix: "", + }, + { + port: 0, + hostname: null, + unix: "", + }, + { + port: 0, + hostname: null, + unix: Buffer.from(""), + }, + { + port: 0, + hostname: Buffer.from(defaultHostname), + unix: Buffer.from(""), + }, + { + port: 0, + hostname: Buffer.from(defaultHostname), + unix: undefined, + }, + ] as const; + + for (const { port, hostname, unix } of permutations) { + test(`port: ${port} and hostname: ${hostname} and unix: ${unix}`, () => { + using server = serve({ + port, + // @ts-expect-error - Testing invalid combination + hostname, + // @ts-expect-error - Testing invalid combination + unix, + fetch() { + return new Response("ok"); + }, + }); + expect(server.port).toBeGreaterThan(0); + expect(server.hostname).toBe((hostname || defaultHostname) + ""); + server.stop(); + }); + } +}); + +describe("Bun.serve error handling", () => { + test("missing fetch handler throws", () => { + // @ts-expect-error - Testing runtime behavior + expect(() => serve({})).toThrow(); + }); + + test("custom error handler", () => { + using server = serve({ + port: 0, + error(error) { + return new Response(`Error: ${error.message}`, { status: 500 }); + }, + fetch() { + throw new Error("test error"); + }, + }); + server.stop(); + }); +}); + +describe("Bun.serve websocket options", () => { + test("basic websocket config", () => { + using server = serve({ + port: 0, + websocket: { + message(ws, message) { + ws.send(message); + }, + }, + fetch(req, server) { + if (server.upgrade(req)) { + return; + } + return new Response("Not a websocket"); + }, + }); + server.stop(); + }); + + test("websocket with all handlers", () => { + using server = serve({ + port: 0, + websocket: { + open(ws) {}, + message(ws, message) {}, + drain(ws) {}, + close(ws, code, reason) {}, + ping(ws, data) {}, + pong(ws, data) {}, + }, + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + }); + + test("websocket with custom limits", () => { + using server = serve({ + port: 0, + websocket: { + message(ws, message) {}, + maxPayloadLength: 1024 * 1024, // 1MB + backpressureLimit: 1024 * 512, // 512KB + closeOnBackpressureLimit: true, + idleTimeout: 60, // 1 minute + }, + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + }); + + test("websocket with compression options", () => { + using server = serve({ + port: 0, + websocket: { + message(ws, message) {}, + perMessageDeflate: { + compress: true, + decompress: "shared", + }, + }, + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + }); +}); + +describe("Bun.serve development options", () => { + test("development mode", () => { + using server = serve({ + development: true, + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.development).toBe(true); + server.stop(); + }); + + test("custom server id", () => { + using server = serve({ + id: "test-server", + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.id).toBe("test-server"); + server.stop(); + }); +}); + +describe("Bun.serve static routes", () => { + test("static route handling", () => { + using server = serve({ + port: 0, + static: { + "/": new Response("Home"), + "/about": new Response("About"), + }, + fetch() { + return new Response("Not found"); + }, + }); + server.stop(); + }); +}); + +describe("Bun.serve unix socket", () => { + test("unix socket config", () => { + const tmpdir = tmpdirSync(); + using server = serve({ + unix: tmpdir + "/test.sock", + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + }); + + test("unix socket with websocket", () => { + const tmpdir = tmpdirSync(); + using server = serve({ + unix: tmpdir + "/test.sock", + websocket: { + message(ws, message) {}, + }, + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + }); +}); + +describe("Bun.serve hostname and port validation", () => { + test("hostname with port 0 gets random port", () => { + using server = serve({ + hostname: "127.0.0.1", + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.port).toBeGreaterThan(0); + expect(server.hostname).toBe("127.0.0.1"); + server.stop(); + }); + + test("port with no hostname gets default hostname", () => { + using server = serve({ + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.port).toBeGreaterThan(0); + expect(server.hostname).toBe(defaultHostname); // Default hostname + server.stop(); + }); + + test("hostname with unix should throw", () => { + expect(() => + serve({ + // @ts-expect-error - Testing invalid combination + hostname: defaultHostname, + unix: "test.sock", + fetch() { + return new Response("ok"); + }, + }), + ).toThrow(); + }); + + test("unix with no hostname/port is valid", () => { + const tmpdir = tmpdirSync(); + using server = serve({ + unix: tmpdir + "/test.sock", + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + }); + + describe("various valid hostnames", () => { + const validHostnames = [defaultHostname, "127.0.0.1", "0.0.0.0"]; + + for (const hostname of validHostnames) { + test(hostname, () => { + using server = serve({ + hostname, + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.hostname).toBe(hostname); + server.stop(); + }); + } + }); + + describe("various port types", () => { + const validPorts = [ + [0, expect.any(Number)], // random port + ["0", expect.any(Number)], // random port as string + ] as const; + + for (const [input, expected] of validPorts) { + test(JSON.stringify(input), () => { + using server = serve({ + port: input, + fetch() { + return new Response("ok"); + }, + }); + + if (typeof expected === "object") { + expect(server.port).toBeGreaterThan(0); + } else { + expect(server.port).toBe(expected); + } + server.stop(); + }); + } + }); +}); + +describe("Bun.serve hostname coercion", () => { + test.todo("number hostnames coerce to string", () => { + using server = serve({ + // @ts-expect-error - Testing runtime coercion + hostname: 0, // Should coerce to "0" + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.hostname).toBe("0"); + server.stop(); + }); + + test("object with toString() coerces to string", () => { + const customHostname = { + toString() { + return defaultHostname; + }, + }; + + using server = serve({ + // @ts-expect-error - Testing runtime coercion + hostname: customHostname, + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.hostname).toBe(defaultHostname); + server.stop(); + }); + + test("invalid toString() results should throw", () => { + const invalidHostnames = [ + { + toString() { + return {}; + }, + }, + { + toString() { + return []; + }, + }, + { + toString() { + return null; + }, + }, + { + toString() { + return undefined; + }, + }, + { + toString() { + throw new Error("invalid toString"); + }, + }, + { + toString() { + return Symbol("test"); + }, + }, + ]; + + for (const hostname of invalidHostnames) { + expect(() => + serve({ + // @ts-expect-error - Testing runtime coercion + hostname, + port: 0, + fetch() { + return new Response("ok"); + }, + }), + ).toThrow(); + } + }); + + test("symbol hostnames should throw", () => { + expect(() => + serve({ + // @ts-expect-error - Testing runtime behavior + hostname: Symbol("test"), + port: 0, + fetch() { + return new Response("ok"); + }, + }), + ).toThrow(); + }); + + test("coerced hostnames must still be valid", () => { + const invalidCoercions = [ + { + toString() { + return "http://example.com"; + }, + }, + { + toString() { + return "example.com:3000"; + }, + }, + { + toString() { + return "-invalid.com"; + }, + }, + ]; + + for (const hostname of invalidCoercions) { + expect(() => + serve({ + // @ts-expect-error - Testing runtime coercion + hostname, + port: 0, + fetch() { + return new Response("ok"); + }, + }), + ).toThrow(); + } + }); + + describe("falsy values should use default or throw", () => { + test("undefined should use default", () => { + using server = serve({ + hostname: undefined, + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.hostname).toBe(defaultHostname); + server.stop(); + }); + + test("null should NOT throw", () => { + expect(() => { + using server = serve({ + // @ts-expect-error - Testing runtime behavior + hostname: null, + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.hostname).toBe(defaultHostname); + }).not.toThrow(); + + test("empty string should be ignored", () => { + expect(() => { + using server = serve({ + hostname: "", + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.hostname).toBe(defaultHostname); + }).not.toThrow(); + }); + }); + }); +}); + +describe("Bun.serve unix socket validation", () => { + test("unix socket with hostname should throw", () => { + expect(() => + serve({ + unix: "/tmp/test.sock", + // @ts-expect-error - Testing invalid combination + hostname: defaultHostname, // Cannot combine with unix + fetch() { + return new Response("ok"); + }, + }), + ).toThrow(); + }); + + describe("invalid unix socket paths should throw", () => { + const invalidPaths = [ + { + toString() { + throw new Error("invalid toString"); + }, + toJSON() { + return "invalid toJSON"; + }, + }, + { + toString() { + return Symbol("test"); + }, + toJSON() { + return "Symbol(test)"; + }, + }, + ]; + + for (const unix of invalidPaths) { + test(JSON.stringify(unix), () => { + expect(() => + serve({ + // @ts-expect-error - Testing invalid unix socket path + unix, + fetch() { + return new Response("ok"); + }, + }), + ).toThrow(); + }); + } + }); + + test("unix socket path coercion", () => { + // Number should coerce to string + using server = serve({ + // @ts-expect-error - Testing runtime coercion + unix: Math.ceil(Math.random() * 100000000), + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + + // Object with toString() + const pathObj = { + toString() { + return Math.random().toString(32).slice(2, 15) + ".sock"; + }, + }; + + using server2 = serve({ + // @ts-expect-error - Testing runtime coercion + unix: pathObj, + fetch() { + return new Response("ok"); + }, + }); + server2.stop(); + }); + + test("invalid unix socket path coercion should throw", () => { + const invalidCoercions = [ + { + toString() { + throw new Error("invalid toString"); + }, + }, + ]; + + for (const unix of invalidCoercions) { + expect(() => { + using server = serve({ + port: 0, + // @ts-expect-error - Testing runtime coercion + unix, + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + }).toThrow(); + } + }); +});