From d9cf836b6785bbb0675ba07cc426d0bed1c242eb Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 3 Jun 2025 01:38:26 -0700 Subject: [PATCH] Split server.zig into more files (#20139) Co-authored-by: Claude Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- cmake/sources/ZigSources.txt | 6 + src/bun.js/api/server.zig | 4858 +---------------- src/bun.js/api/server/AnyRequestContext.zig | 229 + src/bun.js/api/server/HTTPStatusText.zig | 68 + src/bun.js/api/server/RequestContext.zig | 2539 +++++++++ src/bun.js/api/server/SSLConfig.zig | 620 +++ src/bun.js/api/server/ServerConfig.zig | 1092 ++++ src/bun.js/api/server/ServerWebSocket.zig | 2 +- .../api/server/WebSocketServerContext.zig | 322 ++ 9 files changed, 4903 insertions(+), 4833 deletions(-) create mode 100644 src/bun.js/api/server/AnyRequestContext.zig create mode 100644 src/bun.js/api/server/HTTPStatusText.zig create mode 100644 src/bun.js/api/server/RequestContext.zig create mode 100644 src/bun.js/api/server/SSLConfig.zig create mode 100644 src/bun.js/api/server/ServerConfig.zig create mode 100644 src/bun.js/api/server/WebSocketServerContext.zig diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 180597e00f..063fe56a28 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -55,11 +55,17 @@ src/bun.js/api/html_rewriter.zig src/bun.js/api/JSBundler.zig src/bun.js/api/JSTranspiler.zig src/bun.js/api/server.zig +src/bun.js/api/server/AnyRequestContext.zig src/bun.js/api/server/HTMLBundle.zig +src/bun.js/api/server/HTTPStatusText.zig src/bun.js/api/server/InspectorBunFrontendDevServerAgent.zig src/bun.js/api/server/NodeHTTPResponse.zig +src/bun.js/api/server/RequestContext.zig +src/bun.js/api/server/ServerConfig.zig src/bun.js/api/server/ServerWebSocket.zig +src/bun.js/api/server/SSLConfig.zig src/bun.js/api/server/StaticRoute.zig +src/bun.js/api/server/WebSocketServerContext.zig src/bun.js/api/streams.classes.zig src/bun.js/api/Timer.zig src/bun.js/api/TOMLObject.zig diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index e8864f1b4e..028fa4c1e2 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -11,7 +11,6 @@ const Allocator = std.mem.Allocator; const Sys = @import("../../sys.zig"); const logger = bun.logger; -const Api = @import("../../api/schema.zig").Api; const options = @import("../../options.zig"); const Transpiler = bun.Transpiler; const js_printer = bun.js_printer; @@ -29,7 +28,6 @@ const JSValue = bun.JSC.JSValue; const host_fn = JSC.host_fn; const JSGlobalObject = bun.JSC.JSGlobalObject; -const ConsoleObject = bun.JSC.ConsoleObject; const Node = bun.JSC.Node; const JSPromise = bun.JSC.JSPromise; const VM = bun.JSC.VM; @@ -41,128 +39,14 @@ const MimeType = HTTP.MimeType; const Blob = JSC.WebCore.Blob; const BoringSSL = bun.BoringSSL.c; const Arena = @import("../../allocators/mimalloc_arena.zig").Arena; -const SendfileContext = struct { - fd: bun.FileDescriptor, - socket_fd: bun.FileDescriptor = bun.invalid_fd, - remain: Blob.SizeType = 0, - offset: Blob.SizeType = 0, - has_listener: bool = false, - has_set_on_writable: bool = false, - auto_close: bool = false, -}; -const linux = std.os.linux; + const Async = bun.Async; const httplog = Output.scoped(.Server, false); const ctxLog = Output.scoped(.RequestContext, false); -const S3 = bun.S3; const SocketAddress = @import("bun/socket.zig").SocketAddress; -const BlobFileContentResult = struct { - data: [:0]const u8, - - fn init(comptime fieldname: []const u8, js_obj: JSC.JSValue, global: *JSC.JSGlobalObject) bun.JSError!?BlobFileContentResult { - { - const body = try JSC.WebCore.Body.Value.fromJS(global, js_obj); - if (body == .Blob and body.Blob.store != null and body.Blob.store.?.data == .file) { - var fs: JSC.Node.fs.NodeFS = .{}; - const read = fs.readFileWithOptions(.{ .path = body.Blob.store.?.data.file.pathlike }, .sync, .null_terminated); - switch (read) { - .err => { - return global.throwValue(read.err.toJSC(global)); - }, - else => { - const str = read.result.null_terminated; - if (str.len > 0) { - return .{ .data = str }; - } - return global.throwInvalidArguments(std.fmt.comptimePrint("Invalid {s} file", .{fieldname}), .{}); - }, - } - } - } - - return null; - } -}; - -fn getContentType(headers: ?*WebCore.FetchHeaders, blob: *const WebCore.Blob.Any, allocator: std.mem.Allocator) struct { MimeType, bool, bool } { - var needs_content_type = true; - var content_type_needs_free = false; - - const content_type: MimeType = brk: { - if (headers) |headers_| { - if (headers_.fastGet(.ContentType)) |content| { - needs_content_type = false; - - var content_slice = content.toSlice(allocator); - defer content_slice.deinit(); - - const content_type_allocator = if (content_slice.allocator.isNull()) null else allocator; - break :brk MimeType.init(content_slice.slice(), content_type_allocator, &content_type_needs_free); - } - } - - break :brk if (blob.contentType().len > 0) - MimeType.byName(blob.contentType()) - else if (MimeType.sniff(blob.slice())) |content| - content - else if (blob.wasString()) - MimeType.text - // TODO: should we get the mime type off of the Blob.Store if it exists? - // A little wary of doing this right now due to causing some breaking change - else - MimeType.other; - }; - - return .{ content_type, needs_content_type, content_type_needs_free }; -} - -fn validateRouteName(global: *JSC.JSGlobalObject, path: []const u8) !void { - // Already validated by the caller - bun.debugAssert(path.len > 0 and path[0] == '/'); - - // For now, we don't support params that start with a number. - // Mostly because it makes the params object more complicated to implement and it's easier to cut scope this way for now. - var remaining = path; - var duped_route_names = bun.StringHashMap(void).init(bun.default_allocator); - defer duped_route_names.deinit(); - while (strings.indexOfChar(remaining, ':')) |index| { - remaining = remaining[index + 1 ..]; - const end = strings.indexOfChar(remaining, '/') orelse remaining.len; - const route_name = remaining[0..end]; - if (route_name.len > 0 and std.ascii.isDigit(route_name[0])) { - return global.throwTODO( - \\Route parameter names cannot start with a number. - \\ - \\If you run into this, please file an issue and we will add support for it. - ); - } - - const entry = duped_route_names.getOrPut(route_name) catch bun.outOfMemory(); - if (entry.found_existing) { - return global.throwTODO( - \\Support for duplicate route parameter names is not yet implemented. - \\ - \\If you run into this, please file an issue and we will add support for it. - ); - } - - remaining = remaining[end..]; - } -} - -fn writeHeaders( - headers: *WebCore.FetchHeaders, - comptime ssl: bool, - resp_ptr: ?*uws.NewApp(ssl).Response, -) void { - ctxLog("writeHeaders", .{}); - headers.fastRemove(.ContentLength); - headers.fastRemove(.TransferEncoding); - if (resp_ptr) |resp| { - headers.toUWSResponse(ssl, resp); - } -} +pub const WebSocketServerContext = @import("./server/WebSocketServerContext.zig"); +pub const HTTPStatusText = @import("./server/HTTPStatusText.zig"); pub fn writeStatus(comptime ssl: bool, resp_ptr: ?*uws.NewApp(ssl).Response, status: u16) void { if (resp_ptr) |resp| { @@ -241,6 +125,13 @@ pub const AnyRoute = union(enum) { return null; } + pub const ServerInitContext = struct { + arena: std.heap.ArenaAllocator, + dedupe_html_bundle_map: std.AutoHashMap(*HTMLBundle, bun.ptr.RefPtr(HTMLBundle.Route)), + js_string_allocations: bun.bake.StringRefList, + framework_router_list: std.ArrayList(bun.bake.Framework.FileSystemRouterType), + }; + pub fn fromJS( global: *JSC.JSGlobalObject, path: []const u8, @@ -295,4703 +186,7 @@ pub const AnyRoute = union(enum) { } }; -pub const ServerInitContext = struct { - arena: std.heap.ArenaAllocator, - dedupe_html_bundle_map: std.AutoHashMap(*HTMLBundle, bun.ptr.RefPtr(HTMLBundle.Route)), - js_string_allocations: bun.bake.StringRefList, - framework_router_list: std.ArrayList(bun.bake.Framework.FileSystemRouterType), -}; - -const UserRouteBuilder = struct { - route: ServerConfig.RouteDeclaration, - callback: JSC.Strong.Optional = .empty, - - pub fn deinit(this: *UserRouteBuilder) void { - this.route.deinit(); - this.callback.deinit(); - } -}; - -pub const ServerConfig = struct { - address: union(enum) { - tcp: struct { - port: u16 = 0, - hostname: ?[*:0]const u8 = null, - }, - unix: [:0]const u8, - - pub fn deinit(this: *@This(), allocator: std.mem.Allocator) void { - switch (this.*) { - .tcp => |tcp| { - if (tcp.hostname) |host| { - allocator.free(bun.sliceTo(host, 0)); - } - }, - .unix => |addr| { - allocator.free(addr); - }, - } - this.* = .{ .tcp = .{} }; - } - } = .{ - .tcp = .{}, - }, - idleTimeout: u8 = 10, //TODO: should we match websocket default idleTimeout of 120? - has_idleTimeout: bool = false, - // TODO: use webkit URL parser instead of bun's - base_url: URL = URL{}, - base_uri: string = "", - - ssl_config: ?SSLConfig = null, - sni: ?bun.BabyList(SSLConfig) = null, - max_request_body_size: usize = 1024 * 1024 * 128, - development: DevelopmentOption = .development, - broadcast_console_log_from_browser_to_server_for_bake: bool = false, - - /// Enable automatic workspace folders for Chrome DevTools - /// https://chromium.googlesource.com/devtools/devtools-frontend/+/main/docs/ecosystem/automatic_workspace_folders.md - /// https://github.com/ChromeDevTools/vite-plugin-devtools-json/blob/76080b04422b36230d4b7a674b90d6df296cbff5/src/index.ts#L60-L77 - /// - /// If HMR is not enabled, then this field is ignored. - enable_chrome_devtools_automatic_workspace_folders: bool = true, - - onError: JSC.JSValue = JSC.JSValue.zero, - onRequest: JSC.JSValue = JSC.JSValue.zero, - onNodeHTTPRequest: JSC.JSValue = JSC.JSValue.zero, - - websocket: ?WebSocketServer = null, - - inspector: bool = false, - reuse_port: bool = false, - id: []const u8 = "", - allow_hot: bool = true, - ipv6_only: bool = false, - - is_node_http: bool = false, - had_routes_object: bool = false, - - static_routes: std.ArrayList(StaticRouteEntry) = std.ArrayList(StaticRouteEntry).init(bun.default_allocator), - negative_routes: std.ArrayList([:0]const u8) = std.ArrayList([:0]const u8).init(bun.default_allocator), - user_routes_to_build: std.ArrayList(UserRouteBuilder) = std.ArrayList(UserRouteBuilder).init(bun.default_allocator), - - bake: ?bun.bake.UserOptions = null, - - pub const DevelopmentOption = enum { - development, - production, - development_without_hmr, - - pub fn isHMREnabled(this: DevelopmentOption) bool { - return this == .development; - } - - pub fn isDevelopment(this: DevelopmentOption) bool { - return this == .development or this == .development_without_hmr; - } - }; - - pub fn isDevelopment(this: *const ServerConfig) bool { - return this.development.isDevelopment(); - } - - pub fn memoryCost(this: *const ServerConfig) usize { - // ignore @sizeOf(ServerConfig), assume already included. - var cost: usize = 0; - for (this.static_routes.items) |*entry| { - cost += entry.memoryCost(); - } - cost += this.id.len; - cost += this.base_url.href.len; - for (this.negative_routes.items) |route| { - cost += route.len; - } - - return cost; - } - - // We need to be able to apply the route to multiple Apps even when there is only one RouteList. - pub const RouteDeclaration = struct { - path: [:0]const u8 = "", - method: union(enum) { - any: void, - specific: HTTP.Method, - } = .any, - - pub fn deinit(this: *RouteDeclaration) void { - if (this.path.len > 0) { - bun.default_allocator.free(this.path); - } - } - }; - - // TODO: rename to StaticRoute.Entry - pub const StaticRouteEntry = struct { - path: []const u8, - route: AnyRoute, - method: HTTP.Method.Optional = .any, - - pub fn memoryCost(this: *const StaticRouteEntry) usize { - return this.path.len + this.route.memoryCost(); - } - - /// Clone the path buffer and increment the ref count - /// This doesn't actually clone the route, it just increments the ref count - pub fn clone(this: StaticRouteEntry) !StaticRouteEntry { - this.route.ref(); - - return .{ - .path = try bun.default_allocator.dupe(u8, this.path), - .route = this.route, - .method = this.method, - }; - } - - pub fn deinit(this: *StaticRouteEntry) void { - bun.default_allocator.free(this.path); - this.path = ""; - this.route.deref(); - this.* = undefined; - } - - pub fn isLessThan(_: void, this: StaticRouteEntry, other: StaticRouteEntry) bool { - return strings.cmpStringsDesc({}, this.path, other.path); - } - }; - - fn normalizeStaticRoutesList(this: *ServerConfig) !void { - const Context = struct { - // Ac - pub fn hash(route: *StaticRouteEntry) u64 { - var hasher = std.hash.Wyhash.init(0); - switch (route.method) { - .any => hasher.update("ANY"), - .method => |*set| { - var iter = set.iterator(); - while (iter.next()) |method| { - hasher.update(@tagName(method)); - } - }, - } - hasher.update(route.path); - return hasher.final(); - } - }; - - var static_routes_dedupe_list = std.ArrayList(u64).init(bun.default_allocator); - try static_routes_dedupe_list.ensureTotalCapacity(@truncate(this.static_routes.items.len)); - defer static_routes_dedupe_list.deinit(); - - // Iterate through the list of static routes backwards - // Later ones added override earlier ones - var list = &this.static_routes; - if (list.items.len > 0) { - var index = list.items.len - 1; - while (true) { - const route = &list.items[index]; - const hash = Context.hash(route); - if (std.mem.indexOfScalar(u64, static_routes_dedupe_list.items, hash) != null) { - var item = list.orderedRemove(index); - item.deinit(); - } else { - try static_routes_dedupe_list.append(hash); - } - - if (index == 0) break; - index -= 1; - } - } - - // sort the cloned static routes by name for determinism - std.mem.sort(StaticRouteEntry, list.items, {}, StaticRouteEntry.isLessThan); - } - - pub fn cloneForReloadingStaticRoutes(this: *ServerConfig) !ServerConfig { - var that = this.*; - this.ssl_config = null; - this.sni = null; - this.address = .{ .tcp = .{} }; - this.websocket = null; - this.bake = null; - - try that.normalizeStaticRoutesList(); - - return that; - } - - pub fn appendStaticRoute(this: *ServerConfig, path: []const u8, route: AnyRoute, method: HTTP.Method.Optional) !void { - try this.static_routes.append(StaticRouteEntry{ - .path = try bun.default_allocator.dupe(u8, path), - .route = route, - .method = method, - }); - } - - fn applyStaticRoute(server: AnyServer, comptime ssl: bool, app: *uws.NewApp(ssl), comptime T: type, entry: T, path: []const u8, method: HTTP.Method.Optional) void { - entry.server = server; - const handler_wrap = struct { - pub fn handler(route: T, req: *uws.Request, resp: *uws.NewApp(ssl).Response) void { - route.onRequest(req, switch (comptime ssl) { - true => .{ .SSL = resp }, - false => .{ .TCP = resp }, - }); - } - - pub fn HEAD(route: T, req: *uws.Request, resp: *uws.NewApp(ssl).Response) void { - route.onHEADRequest(req, switch (comptime ssl) { - true => .{ .SSL = resp }, - false => .{ .TCP = resp }, - }); - } - }; - app.head(path, T, entry, handler_wrap.HEAD); - switch (method) { - .any => { - app.any(path, T, entry, handler_wrap.handler); - }, - .method => |*m| { - var iter = m.iterator(); - while (iter.next()) |method_| { - app.method(method_, path, T, entry, handler_wrap.handler); - } - }, - } - } - - pub fn deinit(this: *ServerConfig) void { - this.address.deinit(bun.default_allocator); - - for (this.negative_routes.items) |route| { - bun.default_allocator.free(route); - } - this.negative_routes.clearAndFree(); - - if (this.base_url.href.len > 0) { - bun.default_allocator.free(this.base_url.href); - this.base_url = URL{}; - } - if (this.ssl_config) |*ssl_config| { - ssl_config.deinit(); - this.ssl_config = null; - } - if (this.sni) |sni| { - for (sni.slice()) |*ssl_config| { - ssl_config.deinit(); - } - this.sni.?.deinitWithAllocator(bun.default_allocator); - this.sni = null; - } - - for (this.static_routes.items) |*entry| { - entry.deinit(); - } - this.static_routes.clearAndFree(); - - if (this.bake) |*bake| { - bake.deinit(); - } - - for (this.user_routes_to_build.items) |*builder| { - builder.deinit(); - } - this.user_routes_to_build.clearAndFree(); - } - - pub fn computeID(this: *const ServerConfig, allocator: std.mem.Allocator) []const u8 { - var arraylist = std.ArrayList(u8).init(allocator); - var writer = arraylist.writer(); - - writer.writeAll("[http]-") catch {}; - switch (this.address) { - .tcp => { - if (this.address.tcp.hostname) |host| { - writer.print("tcp:{s}:{d}", .{ - bun.sliceTo(host, 0), - this.address.tcp.port, - }) catch {}; - } else { - writer.print("tcp:localhost:{d}", .{ - this.address.tcp.port, - }) catch {}; - } - }, - .unix => { - writer.print("unix:{s}", .{ - bun.sliceTo(this.address.unix, 0), - }) catch {}; - }, - } - - return arraylist.items; - } - - pub fn getUsocketsOptions(this: *const ServerConfig) i32 { - // Unlike Node.js, we set exclusive port in case reuse port is not set - var out: i32 = if (this.reuse_port) - uws.LIBUS_LISTEN_REUSE_PORT | uws.LIBUS_LISTEN_REUSE_ADDR - else - uws.LIBUS_LISTEN_EXCLUSIVE_PORT; - - if (this.ipv6_only) { - out |= uws.LIBUS_SOCKET_IPV6_ONLY; - } - - return out; - } - - pub const SSLConfig = struct { - requires_custom_request_ctx: bool = false, - server_name: [*c]const u8 = null, - - key_file_name: [*c]const u8 = null, - cert_file_name: [*c]const u8 = null, - - ca_file_name: [*c]const u8 = null, - dh_params_file_name: [*c]const u8 = null, - - passphrase: [*c]const u8 = null, - low_memory_mode: bool = false, - - key: ?[][*c]const u8 = null, - key_count: u32 = 0, - - cert: ?[][*c]const u8 = null, - cert_count: u32 = 0, - - ca: ?[][*c]const u8 = null, - ca_count: u32 = 0, - - secure_options: u32 = 0, - request_cert: i32 = 0, - reject_unauthorized: i32 = 0, - ssl_ciphers: ?[*:0]const u8 = null, - protos: ?[*:0]const u8 = null, - protos_len: usize = 0, - client_renegotiation_limit: u32 = 0, - client_renegotiation_window: u32 = 0, - - const log = Output.scoped(.SSLConfig, false); - - pub fn asUSockets(this: SSLConfig) uws.SocketContext.BunSocketContextOptions { - var ctx_opts: uws.SocketContext.BunSocketContextOptions = .{}; - - if (this.key_file_name != null) - ctx_opts.key_file_name = this.key_file_name; - if (this.cert_file_name != null) - ctx_opts.cert_file_name = this.cert_file_name; - if (this.ca_file_name != null) - ctx_opts.ca_file_name = this.ca_file_name; - if (this.dh_params_file_name != null) - ctx_opts.dh_params_file_name = this.dh_params_file_name; - if (this.passphrase != null) - ctx_opts.passphrase = this.passphrase; - ctx_opts.ssl_prefer_low_memory_usage = @intFromBool(this.low_memory_mode); - - if (this.key) |key| { - ctx_opts.key = key.ptr; - ctx_opts.key_count = this.key_count; - } - if (this.cert) |cert| { - ctx_opts.cert = cert.ptr; - ctx_opts.cert_count = this.cert_count; - } - if (this.ca) |ca| { - ctx_opts.ca = ca.ptr; - ctx_opts.ca_count = this.ca_count; - } - - if (this.ssl_ciphers != null) { - ctx_opts.ssl_ciphers = this.ssl_ciphers; - } - ctx_opts.request_cert = this.request_cert; - ctx_opts.reject_unauthorized = this.reject_unauthorized; - - return ctx_opts; - } - - pub fn isSame(thisConfig: *const SSLConfig, otherConfig: *const SSLConfig) bool { - { //strings - const fields = .{ - "server_name", - "key_file_name", - "cert_file_name", - "ca_file_name", - "dh_params_file_name", - "passphrase", - "ssl_ciphers", - "protos", - }; - - inline for (fields) |field| { - const lhs = @field(thisConfig, field); - const rhs = @field(otherConfig, field); - if (lhs != null and rhs != null) { - if (!stringsEqual(lhs, rhs)) - return false; - } else if (lhs != null or rhs != null) { - return false; - } - } - } - - { - //numbers - const fields = .{ "secure_options", "request_cert", "reject_unauthorized", "low_memory_mode" }; - - inline for (fields) |field| { - const lhs = @field(thisConfig, field); - const rhs = @field(otherConfig, field); - if (lhs != rhs) - return false; - } - } - - { - // complex fields - const fields = .{ "key", "ca", "cert" }; - inline for (fields) |field| { - const lhs_count = @field(thisConfig, field ++ "_count"); - const rhs_count = @field(otherConfig, field ++ "_count"); - if (lhs_count != rhs_count) - return false; - if (lhs_count > 0) { - const lhs = @field(thisConfig, field); - const rhs = @field(otherConfig, field); - for (0..lhs_count) |i| { - if (!stringsEqual(lhs.?[i], rhs.?[i])) - return false; - } - } - } - } - - return true; - } - - fn stringsEqual(a: [*c]const u8, b: [*c]const u8) bool { - const lhs = bun.asByteSlice(a); - const rhs = bun.asByteSlice(b); - return strings.eqlLong(lhs, rhs, true); - } - - pub fn deinit(this: *SSLConfig) void { - const fields = .{ - "server_name", - "key_file_name", - "cert_file_name", - "ca_file_name", - "dh_params_file_name", - "passphrase", - "ssl_ciphers", - "protos", - }; - - inline for (fields) |field| { - if (@field(this, field)) |slice_ptr| { - const slice = std.mem.span(slice_ptr); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - @field(this, field) = ""; - } - } - - if (this.cert) |cert| { - for (0..this.cert_count) |i| { - const slice = std.mem.span(cert[i]); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - } - - bun.default_allocator.free(cert); - this.cert = null; - } - - if (this.key) |key| { - for (0..this.key_count) |i| { - const slice = std.mem.span(key[i]); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - } - - bun.default_allocator.free(key); - this.key = null; - } - - if (this.ca) |ca| { - for (0..this.ca_count) |i| { - const slice = std.mem.span(ca[i]); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - } - - bun.default_allocator.free(ca); - this.ca = null; - } - } - - pub const zero = SSLConfig{}; - - pub fn fromJS(vm: *JSC.VirtualMachine, global: *JSC.JSGlobalObject, obj: JSC.JSValue) bun.JSError!?SSLConfig { - var result = zero; - errdefer result.deinit(); - - var arena: bun.ArenaAllocator = bun.ArenaAllocator.init(bun.default_allocator); - defer arena.deinit(); - - if (!obj.isObject()) { - return global.throwInvalidArguments("tls option expects an object", .{}); - } - - var any = false; - - result.reject_unauthorized = @intFromBool(vm.getTLSRejectUnauthorized()); - - // Required - if (try obj.getTruthy(global, "keyFile")) |key_file_name| { - var sliced = try key_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.key_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.key_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Unable to access keyFile path", .{}); - } - any = true; - result.requires_custom_request_ctx = true; - } - } - - if (try obj.getTruthy(global, "key")) |js_obj| { - if (js_obj.jsType().isArray()) { - const count = js_obj.getLength(global); - if (count > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, count); - - var valid_count: u32 = 0; - for (0..count) |i| { - const item = js_obj.getIndex(global, @intCast(i)); - if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[valid_count] = try bun.default_allocator.dupeZ(u8, sliced); - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } - } else if (try BlobFileContentResult.init("key", item, global)) |content| { - if (content.data.len > 0) { - native_array[valid_count] = content.data.ptr; - valid_count += 1; - result.requires_custom_request_ctx = true; - any = true; - } else { - // mark and free all CA's - result.cert = native_array; - result.deinit(); - return null; - } - } else { - // mark and free all keys - result.key = native_array; - return global.throwInvalidArguments("key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - - if (valid_count == 0) { - bun.default_allocator.free(native_array); - } else { - result.key = native_array; - } - - result.key_count = valid_count; - } - } else if (try BlobFileContentResult.init("key", js_obj, global)) |content| { - if (content.data.len > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - native_array[0] = content.data.ptr; - result.key = native_array; - result.key_count = 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - result.deinit(); - return null; - } - } else { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[0] = try bun.default_allocator.dupeZ(u8, sliced); - any = true; - result.requires_custom_request_ctx = true; - result.key = native_array; - result.key_count = 1; - } else { - bun.default_allocator.free(native_array); - } - } else { - // mark and free all certs - result.key = native_array; - return global.throwInvalidArguments("key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - } - - if (try obj.getTruthy(global, "certFile")) |cert_file_name| { - var sliced = try cert_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.cert_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.cert_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Unable to access certFile path", .{}); - } - any = true; - result.requires_custom_request_ctx = true; - } - } - - if (try obj.getTruthy(global, "ALPNProtocols")) |protocols| { - if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), protocols)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - result.protos = try bun.default_allocator.dupeZ(u8, sliced); - result.protos_len = sliced.len; - } - - any = true; - result.requires_custom_request_ctx = true; - } else { - return global.throwInvalidArguments("ALPNProtocols argument must be an string, Buffer or TypedArray", .{}); - } - } - - if (try obj.getTruthy(global, "cert")) |js_obj| { - if (js_obj.jsType().isArray()) { - const count = js_obj.getLength(global); - if (count > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, count); - - var valid_count: u32 = 0; - for (0..count) |i| { - const item = js_obj.getIndex(global, @intCast(i)); - if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[valid_count] = try bun.default_allocator.dupeZ(u8, sliced); - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } - } else if (try BlobFileContentResult.init("cert", item, global)) |content| { - if (content.data.len > 0) { - native_array[valid_count] = content.data.ptr; - valid_count += 1; - result.requires_custom_request_ctx = true; - any = true; - } else { - // mark and free all CA's - result.cert = native_array; - result.deinit(); - return null; - } - } else { - // mark and free all certs - result.cert = native_array; - return global.throwInvalidArguments("cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - - if (valid_count == 0) { - bun.default_allocator.free(native_array); - } else { - result.cert = native_array; - } - - result.cert_count = valid_count; - } - } else if (try BlobFileContentResult.init("cert", js_obj, global)) |content| { - if (content.data.len > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - native_array[0] = content.data.ptr; - result.cert = native_array; - result.cert_count = 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - result.deinit(); - return null; - } - } else { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[0] = try bun.default_allocator.dupeZ(u8, sliced); - any = true; - result.requires_custom_request_ctx = true; - result.cert = native_array; - result.cert_count = 1; - } else { - bun.default_allocator.free(native_array); - } - } else { - // mark and free all certs - result.cert = native_array; - return global.throwInvalidArguments("cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - } - - if (try obj.getBooleanStrict(global, "requestCert")) |request_cert| { - result.request_cert = if (request_cert) 1 else 0; - any = true; - } - - if (try obj.getBooleanStrict(global, "rejectUnauthorized")) |reject_unauthorized| { - result.reject_unauthorized = if (reject_unauthorized) 1 else 0; - any = true; - } - - if (try obj.getTruthy(global, "ciphers")) |ssl_ciphers| { - var sliced = try ssl_ciphers.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.ssl_ciphers = try bun.default_allocator.dupeZ(u8, sliced.slice()); - any = true; - result.requires_custom_request_ctx = true; - } - } - - if (try obj.getTruthy(global, "serverName") orelse try obj.getTruthy(global, "servername")) |server_name| { - var sliced = try server_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.server_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - any = true; - result.requires_custom_request_ctx = true; - } - } - - if (try obj.getTruthy(global, "ca")) |js_obj| { - if (js_obj.jsType().isArray()) { - const count = js_obj.getLength(global); - if (count > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, count); - - var valid_count: u32 = 0; - for (0..count) |i| { - const item = js_obj.getIndex(global, @intCast(i)); - if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[valid_count] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable; - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } - } else if (try BlobFileContentResult.init("ca", item, global)) |content| { - if (content.data.len > 0) { - native_array[valid_count] = content.data.ptr; - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - // mark and free all CA's - result.cert = native_array; - result.deinit(); - return null; - } - } else { - // mark and free all CA's - result.cert = native_array; - return global.throwInvalidArguments("ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - - if (valid_count == 0) { - bun.default_allocator.free(native_array); - } else { - result.ca = native_array; - } - - result.ca_count = valid_count; - } - } else if (try BlobFileContentResult.init("ca", js_obj, global)) |content| { - if (content.data.len > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - native_array[0] = content.data.ptr; - result.ca = native_array; - result.ca_count = 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - result.deinit(); - return null; - } - } else { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[0] = try bun.default_allocator.dupeZ(u8, sliced); - any = true; - result.requires_custom_request_ctx = true; - result.ca = native_array; - result.ca_count = 1; - } else { - bun.default_allocator.free(native_array); - } - } else { - // mark and free all certs - result.ca = native_array; - return global.throwInvalidArguments("ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - } - - if (try obj.getTruthy(global, "caFile")) |ca_file_name| { - var sliced = try ca_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.ca_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.ca_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Invalid caFile path", .{}); - } - } - } - // Optional - if (any) { - if (try obj.getTruthy(global, "secureOptions")) |secure_options| { - if (secure_options.isNumber()) { - result.secure_options = secure_options.toU32(); - } - } - - if (try obj.getTruthy(global, "clientRenegotiationLimit")) |client_renegotiation_limit| { - if (client_renegotiation_limit.isNumber()) { - result.client_renegotiation_limit = client_renegotiation_limit.toU32(); - } - } - - if (try obj.getTruthy(global, "clientRenegotiationWindow")) |client_renegotiation_window| { - if (client_renegotiation_window.isNumber()) { - result.client_renegotiation_window = client_renegotiation_window.toU32(); - } - } - - if (try obj.getTruthy(global, "dhParamsFile")) |dh_params_file_name| { - var sliced = try dh_params_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.dh_params_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.dh_params_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Invalid dhParamsFile path", .{}); - } - } - } - - if (try obj.getTruthy(global, "passphrase")) |passphrase| { - var sliced = try passphrase.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.passphrase = try bun.default_allocator.dupeZ(u8, sliced.slice()); - } - } - - if (try obj.get(global, "lowMemoryMode")) |low_memory_mode| { - if (low_memory_mode.isBoolean() or low_memory_mode.isUndefined()) { - result.low_memory_mode = low_memory_mode.toBoolean(); - any = true; - } else { - return global.throw("Expected lowMemoryMode to be a boolean", .{}); - } - } - } - - if (!any) - return null; - return result; - } - }; - - fn getRoutesObject(global: *JSC.JSGlobalObject, arg: JSC.JSValue) bun.JSError!?JSC.JSValue { - inline for (.{ "routes", "static" }) |key| { - if (try arg.get(global, key)) |routes| { - // https://github.com/oven-sh/bun/issues/17568 - if (routes.isArray()) { - return null; - } - return routes; - } - } - return null; - } - - pub const FromJSOptions = struct { - allow_bake_config: bool = true, - is_fetch_required: bool = true, - has_user_routes: bool = false, - }; - - pub fn fromJS( - global: *JSC.JSGlobalObject, - args: *ServerConfig, - arguments: *JSC.CallFrame.ArgumentsSlice, - opts: FromJSOptions, - ) bun.JSError!void { - const vm = arguments.vm; - const env = vm.transpiler.env; - - args.* = .{ - .address = .{ - .tcp = .{ - .port = 3000, - .hostname = null, - }, - }, - .development = if (vm.transpiler.options.transform_options.serve_hmr) |hmr| - if (!hmr) .development_without_hmr else .development - else - .development, - - // If this is a node:cluster child, let's default to SO_REUSEPORT. - // That way you don't have to remember to set reusePort: true in Bun.serve() when using node:cluster. - .reuse_port = env.get("NODE_UNIQUE_ID") != null, - }; - var has_hostname = false; - - defer { - if (!args.development.isHMREnabled()) { - bun.assert(args.bake == null); - } - } - - if (strings.eqlComptime(env.get("NODE_ENV") orelse "", "production")) { - args.development = .production; - } - - if (arguments.vm.transpiler.options.production) { - args.development = .production; - } - - args.address.tcp.port = brk: { - const PORT_ENV = .{ "BUN_PORT", "PORT", "NODE_PORT" }; - - inline for (PORT_ENV) |PORT| { - if (env.get(PORT)) |port| { - if (std.fmt.parseInt(u16, port, 10)) |_port| { - break :brk _port; - } else |_| {} - } - } - - if (arguments.vm.transpiler.options.transform_options.port) |port| { - break :brk port; - } - - break :brk args.address.tcp.port; - }; - var port = args.address.tcp.port; - - if (arguments.vm.transpiler.options.transform_options.origin) |origin| { - args.base_uri = try bun.default_allocator.dupeZ(u8, origin); - } - - defer { - if (global.hasException()) { - if (args.ssl_config) |*conf| { - conf.deinit(); - args.ssl_config = null; - } - } - } - - if (arguments.next()) |arg| { - if (!arg.isObject()) { - return global.throwInvalidArguments("Bun.serve expects an object", .{}); - } - - // "development" impacts other settings like bake. - if (try arg.get(global, "development")) |dev| { - if (dev.isObject()) { - if (try dev.getBooleanStrict(global, "hmr")) |hmr| { - args.development = if (!hmr) .development_without_hmr else .development; - } else { - args.development = .development; - } - - if (try dev.getBooleanStrict(global, "console")) |console| { - args.broadcast_console_log_from_browser_to_server_for_bake = console; - } - - if (try dev.getBooleanStrict(global, "chromeDevToolsAutomaticWorkspaceFolders")) |enable_chrome_devtools_automatic_workspace_folders| { - args.enable_chrome_devtools_automatic_workspace_folders = enable_chrome_devtools_automatic_workspace_folders; - } - } else { - args.development = if (dev.toBoolean()) .development else .production; - } - args.reuse_port = args.development == .production; - } - if (global.hasException()) return error.JSError; - - if (try getRoutesObject(global, arg)) |static| { - const static_obj = static.getObject() orelse { - return global.throwInvalidArguments( - \\Bun.serve() expects 'routes' to be an object shaped like: - \\ - \\ { - \\ "/path": { - \\ GET: (req) => new Response("Hello"), - \\ POST: (req) => new Response("Hello"), - \\ }, - \\ "/path2/:param": new Response("Hello"), - \\ "/path3/:param1/:param2": (req) => new Response("Hello") - \\ } - \\ - \\Learn more at https://bun.sh/docs/api/http - , .{}); - }; - args.had_routes_object = true; - - var iter = try JSC.JSPropertyIterator(.{ - .skip_empty_name = true, - .include_value = true, - }).init(global, static_obj); - defer iter.deinit(); - - var init_ctx: ServerInitContext = .{ - .arena = .init(bun.default_allocator), - .dedupe_html_bundle_map = .init(bun.default_allocator), - .framework_router_list = .init(bun.default_allocator), - .js_string_allocations = .empty, - }; - errdefer { - init_ctx.arena.deinit(); - init_ctx.framework_router_list.deinit(); - } - // This list is not used in the success case - defer init_ctx.dedupe_html_bundle_map.deinit(); - - var framework_router_list = std.ArrayList(bun.bake.FrameworkRouter.Type).init(bun.default_allocator); - errdefer framework_router_list.deinit(); - - errdefer { - for (args.static_routes.items) |*static_route| { - static_route.deinit(); - } - args.static_routes.clearAndFree(); - } - - while (try iter.next()) |key| { - const path, const is_ascii = key.toOwnedSliceReturningAllASCII(bun.default_allocator) catch bun.outOfMemory(); - errdefer bun.default_allocator.free(path); - - const value: JSC.JSValue = iter.value; - - if (value.isUndefined()) { - continue; - } - - if (path.len == 0 or (path[0] != '/')) { - return global.throwInvalidArguments("Invalid route {}. Path must start with '/'", .{bun.fmt.quote(path)}); - } - - if (!is_ascii) { - return global.throwInvalidArguments("Invalid route {}. Please encode all non-ASCII characters in the path.", .{bun.fmt.quote(path)}); - } - - if (value == .false) { - const duped = bun.default_allocator.dupeZ(u8, path) catch bun.outOfMemory(); - defer bun.default_allocator.free(path); - args.negative_routes.append(duped) catch bun.outOfMemory(); - continue; - } - - if (value.isCallable()) { - try validateRouteName(global, path); - args.user_routes_to_build.append(.{ - .route = .{ - .path = bun.default_allocator.dupeZ(u8, path) catch bun.outOfMemory(), - .method = .any, - }, - .callback = .create(value.withAsyncContextIfNeeded(global), global), - }) catch bun.outOfMemory(); - bun.default_allocator.free(path); - continue; - } else if (value.isObject()) { - const methods = .{ - HTTP.Method.CONNECT, - HTTP.Method.DELETE, - HTTP.Method.GET, - HTTP.Method.HEAD, - HTTP.Method.OPTIONS, - HTTP.Method.PATCH, - HTTP.Method.POST, - HTTP.Method.PUT, - HTTP.Method.TRACE, - }; - var found = false; - inline for (methods) |method| { - if (value.getOwn(global, @tagName(method))) |function| { - if (!found) { - try validateRouteName(global, path); - } - found = true; - - if (function.isCallable()) { - args.user_routes_to_build.append(.{ - .route = .{ - .path = bun.default_allocator.dupeZ(u8, path) catch bun.outOfMemory(), - .method = .{ .specific = method }, - }, - .callback = .create(function.withAsyncContextIfNeeded(global), global), - }) catch bun.outOfMemory(); - } else if (try AnyRoute.fromJS(global, path, function, &init_ctx)) |html_route| { - var method_set = bun.http.Method.Set.initEmpty(); - method_set.insert(method); - - args.static_routes.append(.{ - .path = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(), - .route = html_route, - .method = .{ .method = method_set }, - }) catch bun.outOfMemory(); - } - } - } - - if (found) { - bun.default_allocator.free(path); - continue; - } - } - - const route = try AnyRoute.fromJS(global, path, value, &init_ctx) orelse { - return global.throwInvalidArguments( - \\'routes' expects a Record Response|Promise}> - \\ - \\To bundle frontend apps on-demand with Bun.serve(), import HTML files. - \\ - \\Example: - \\ - \\```js - \\import { serve } from "bun"; - \\import app from "./app.html"; - \\ - \\serve({ - \\ routes: { - \\ "/index.json": Response.json({ message: "Hello World" }), - \\ "/app": app, - \\ "/path/:param": (req) => { - \\ const param = req.params.param; - \\ return Response.json({ message: `Hello ${param}` }); - \\ }, - \\ "/path": { - \\ GET(req) { - \\ return Response.json({ message: "Hello World" }); - \\ }, - \\ POST(req) { - \\ return Response.json({ message: "Hello World" }); - \\ }, - \\ }, - \\ }, - \\ - \\ fetch(request) { - \\ return new Response("fallback response"); - \\ }, - \\}); - \\``` - \\ - \\See https://bun.sh/docs/api/http for more information. - , - .{}, - ); - }; - args.static_routes.append(.{ - .path = path, - .route = route, - }) catch bun.outOfMemory(); - } - - // When HTML bundles are provided, ensure DevServer options are ready - // The presence of these options causes Bun.serve to initialize things. - if ((init_ctx.dedupe_html_bundle_map.count() > 0 or - init_ctx.framework_router_list.items.len > 0)) - { - if (args.development.isHMREnabled()) { - const root = bun.fs.FileSystem.instance.top_level_dir; - const framework = try bun.bake.Framework.auto( - init_ctx.arena.allocator(), - &global.bunVM().transpiler.resolver, - init_ctx.framework_router_list.items, - ); - args.bake = .{ - .arena = init_ctx.arena, - .allocations = init_ctx.js_string_allocations, - .root = root, - .framework = framework, - .bundler_options = bun.bake.SplitBundlerOptions.empty, - }; - const bake = &args.bake.?; - - const o = vm.transpiler.options.transform_options; - - switch (o.serve_env_behavior) { - .prefix => { - bake.bundler_options.client.env_prefix = vm.transpiler.options.transform_options.serve_env_prefix; - bake.bundler_options.client.env = .prefix; - }, - .load_all => { - bake.bundler_options.client.env = .load_all; - }, - .disable => { - bake.bundler_options.client.env = .disable; - }, - else => {}, - } - - if (o.serve_define) |define| { - bake.bundler_options.client.define = define; - bake.bundler_options.server.define = define; - bake.bundler_options.ssr.define = define; - } - } else { - if (init_ctx.framework_router_list.items.len > 0) { - return global.throwInvalidArguments("FrameworkRouter is currently only supported when `development: true`", .{}); - } - init_ctx.arena.deinit(); - } - } else { - bun.debugAssert(init_ctx.arena.state.end_index == 0 and - init_ctx.arena.state.buffer_list.first == null); - init_ctx.arena.deinit(); - } - } - - if (global.hasException()) return error.JSError; - - if (try arg.get(global, "idleTimeout")) |value| { - if (!value.isUndefinedOrNull()) { - if (!value.isAnyInt()) { - return global.throwInvalidArguments("Bun.serve expects idleTimeout to be an integer", .{}); - } - args.has_idleTimeout = true; - - const idleTimeout: u64 = @intCast(@max(value.toInt64(), 0)); - if (idleTimeout > 255) { - return global.throwInvalidArguments("Bun.serve expects idleTimeout to be 255 or less", .{}); - } - - args.idleTimeout = @truncate(idleTimeout); - } - } - - if (try arg.getTruthy(global, "webSocket") orelse try arg.getTruthy(global, "websocket")) |websocket_object| { - if (!websocket_object.isObject()) { - if (args.ssl_config) |*conf| { - conf.deinit(); - } - return global.throwInvalidArguments("Expected websocket to be an object", .{}); - } - - errdefer if (args.ssl_config) |*conf| conf.deinit(); - args.websocket = try WebSocketServer.onCreate(global, websocket_object); - } - if (global.hasException()) return error.JSError; - - if (try arg.getTruthy(global, "port")) |port_| { - args.address.tcp.port = @as( - u16, - @intCast(@min( - @max(0, port_.coerce(i32, global)), - std.math.maxInt(u16), - )), - ); - port = args.address.tcp.port; - } - if (global.hasException()) return error.JSError; - - if (try arg.getTruthy(global, "baseURI")) |baseURI| { - var sliced = try baseURI.toSlice(global, bun.default_allocator); - - if (sliced.len > 0) { - defer sliced.deinit(); - if (args.base_uri.len > 0) { - bun.default_allocator.free(@constCast(args.base_uri)); - } - args.base_uri = bun.default_allocator.dupe(u8, sliced.slice()) catch unreachable; - } - } - if (global.hasException()) return error.JSError; - - 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) { - args.address.tcp.hostname = bun.default_allocator.dupeZ(u8, host_str.slice()) catch unreachable; - has_hostname = true; - } - } - if (global.hasException()) return error.JSError; - - 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) { - return global.throwInvalidArguments("Cannot specify both hostname and unix", .{}); - } - - args.address = .{ .unix = bun.default_allocator.dupeZ(u8, unix_str.slice()) catch unreachable }; - } - } - if (global.hasException()) return error.JSError; - - if (try arg.get(global, "id")) |id| { - if (id.isUndefinedOrNull()) { - args.allow_hot = false; - } else { - const id_str = try id.toSlice( - global, - bun.default_allocator, - ); - - if (id_str.len > 0) { - args.id = (id_str.cloneIfNeeded(bun.default_allocator) catch unreachable).slice(); - } else { - args.allow_hot = false; - } - } - } - if (global.hasException()) return error.JSError; - - if (opts.allow_bake_config) { - if (try arg.getTruthy(global, "app")) |bake_args_js| brk: { - if (!bun.FeatureFlags.bake()) { - break :brk; - } - if (args.bake != null) { - // "app" is likely to be removed in favor of the HTML loader. - return global.throwInvalidArguments("'app' + HTML loader not supported.", .{}); - } - - if (args.development == .production) { - return global.throwInvalidArguments("TODO: 'development: false' in serve options with 'app'. For now, use `bun build --app` or set 'development: true'", .{}); - } - - args.bake = try bun.bake.UserOptions.fromJS(bake_args_js, global); - } - } - - if (try arg.get(global, "reusePort")) |dev| { - args.reuse_port = dev.coerce(bool, global); - } - if (global.hasException()) return error.JSError; - - if (try arg.get(global, "ipv6Only")) |dev| { - args.ipv6_only = dev.coerce(bool, global); - } - if (global.hasException()) return error.JSError; - - if (try arg.get(global, "inspector")) |inspector| { - args.inspector = inspector.coerce(bool, global); - - if (args.inspector and args.development == .production) { - return global.throwInvalidArguments("Cannot enable inspector in production. Please set development: true in Bun.serve()", .{}); - } - } - if (global.hasException()) return error.JSError; - - if (try arg.getTruthy(global, "maxRequestBodySize")) |max_request_body_size| { - if (max_request_body_size.isNumber()) { - args.max_request_body_size = @as(u64, @intCast(@max(0, max_request_body_size.toInt64()))); - } - } - if (global.hasException()) return error.JSError; - - if (try arg.getTruthyComptime(global, "error")) |onError| { - if (!onError.isCallable()) { - return global.throwInvalidArguments("Expected error to be a function", .{}); - } - const onErrorSnapshot = onError.withAsyncContextIfNeeded(global); - args.onError = onErrorSnapshot; - onErrorSnapshot.protect(); - } - if (global.hasException()) return error.JSError; - - if (try arg.getTruthy(global, "onNodeHTTPRequest")) |onRequest_| { - if (!onRequest_.isCallable()) { - return global.throwInvalidArguments("Expected onNodeHTTPRequest to be a function", .{}); - } - const onRequest = onRequest_.withAsyncContextIfNeeded(global); - onRequest.protect(); - args.onNodeHTTPRequest = onRequest; - } - - if (try arg.getTruthy(global, "fetch")) |onRequest_| { - if (!onRequest_.isCallable()) { - return global.throwInvalidArguments("Expected fetch() to be a function", .{}); - } - const onRequest = onRequest_.withAsyncContextIfNeeded(global); - onRequest.protect(); - args.onRequest = onRequest; - } else if (args.bake == null and args.onNodeHTTPRequest == .zero and ((args.static_routes.items.len + args.user_routes_to_build.items.len) == 0 and !opts.has_user_routes) and opts.is_fetch_required) { - if (global.hasException()) return error.JSError; - return global.throwInvalidArguments( - \\Bun.serve() needs either: - \\ - \\ - A routes object: - \\ routes: { - \\ "/path": { - \\ GET: (req) => new Response("Hello") - \\ } - \\ } - \\ - \\ - Or a fetch handler: - \\ fetch: (req) => { - \\ return new Response("Hello") - \\ } - \\ - \\Learn more at https://bun.sh/docs/api/http - , .{}); - } else { - if (global.hasException()) return error.JSError; - } - - if (try arg.getTruthy(global, "tls")) |tls| { - if (tls.isFalsey()) { - args.ssl_config = null; - } else if (tls.jsType().isArray()) { - var value_iter = tls.arrayIterator(global); - if (value_iter.len == 1) { - return global.throwInvalidArguments("tls option expects at least 1 tls object", .{}); - } - while (value_iter.next()) |item| { - var ssl_config = try SSLConfig.fromJS(vm, global, item) orelse { - if (global.hasException()) { - return error.JSError; - } - - // Backwards-compatibility; we ignored empty tls objects. - continue; - }; - - if (args.ssl_config == null) { - args.ssl_config = ssl_config; - } else { - if (ssl_config.server_name == null or std.mem.span(ssl_config.server_name).len == 0) { - defer ssl_config.deinit(); - return global.throwInvalidArguments("SNI tls object must have a serverName", .{}); - } - if (args.sni == null) { - args.sni = bun.BabyList(SSLConfig).initCapacity(bun.default_allocator, value_iter.len - 1) catch bun.outOfMemory(); - } - - args.sni.?.push(bun.default_allocator, ssl_config) catch bun.outOfMemory(); - } - } - } else { - if (try SSLConfig.fromJS(vm, global, tls)) |ssl_config| { - args.ssl_config = ssl_config; - } - if (global.hasException()) { - return error.JSError; - } - } - } - if (global.hasException()) return error.JSError; - - // @compatibility Bun v0.x - v0.2.1 - // this used to be top-level, now it's "tls" object - if (args.ssl_config == null) { - if (try SSLConfig.fromJS(vm, global, arg)) |ssl_config| { - args.ssl_config = ssl_config; - } - if (global.hasException()) { - return error.JSError; - } - } - } else { - return global.throwInvalidArguments("Bun.serve expects an object", .{}); - } - - if (args.base_uri.len > 0) { - args.base_url = URL.parse(args.base_uri); - if (args.base_url.hostname.len == 0) { - bun.default_allocator.free(@constCast(args.base_uri)); - args.base_uri = ""; - return global.throwInvalidArguments("baseURI must have a hostname", .{}); - } - - if (!strings.isAllASCII(args.base_uri)) { - bun.default_allocator.free(@constCast(args.base_uri)); - args.base_uri = ""; - return global.throwInvalidArguments("Unicode baseURI must already be encoded for now.\nnew URL(baseuRI).toString() should do the trick.", .{}); - } - - if (args.base_url.protocol.len == 0) { - const protocol: string = if (args.ssl_config != null) "https" else "http"; - const hostname = args.base_url.hostname; - const needsBrackets: bool = strings.isIPV6Address(hostname) and hostname[0] != '['; - const original_base_uri = args.base_uri; - defer bun.default_allocator.free(@constCast(original_base_uri)); - if (needsBrackets) { - args.base_uri = (if ((port == 80 and args.ssl_config == null) or (port == 443 and args.ssl_config != null)) - std.fmt.allocPrint(bun.default_allocator, "{s}://[{s}]/{s}", .{ - protocol, - hostname, - strings.trimLeadingChar(args.base_url.pathname, '/'), - }) - else - std.fmt.allocPrint(bun.default_allocator, "{s}://[{s}]:{d}/{s}", .{ - protocol, - hostname, - port, - strings.trimLeadingChar(args.base_url.pathname, '/'), - })) catch unreachable; - } else { - args.base_uri = (if ((port == 80 and args.ssl_config == null) or (port == 443 and args.ssl_config != null)) - std.fmt.allocPrint(bun.default_allocator, "{s}://{s}/{s}", .{ - protocol, - hostname, - strings.trimLeadingChar(args.base_url.pathname, '/'), - }) - else - std.fmt.allocPrint(bun.default_allocator, "{s}://{s}:{d}/{s}", .{ - protocol, - hostname, - port, - strings.trimLeadingChar(args.base_url.pathname, '/'), - })) catch unreachable; - } - - args.base_url = URL.parse(args.base_uri); - } - } else { - const hostname: string = - if (has_hostname) std.mem.span(args.address.tcp.hostname.?) else "0.0.0.0"; - - const needsBrackets: bool = strings.isIPV6Address(hostname) and hostname[0] != '['; - - const protocol: string = if (args.ssl_config != null) "https" else "http"; - if (needsBrackets) { - args.base_uri = (if ((port == 80 and args.ssl_config == null) or (port == 443 and args.ssl_config != null)) - std.fmt.allocPrint(bun.default_allocator, "{s}://[{s}]/", .{ - protocol, - hostname, - }) - else - std.fmt.allocPrint(bun.default_allocator, "{s}://[{s}]:{d}/", .{ protocol, hostname, port })) catch unreachable; - } else { - args.base_uri = (if ((port == 80 and args.ssl_config == null) or (port == 443 and args.ssl_config != null)) - std.fmt.allocPrint(bun.default_allocator, "{s}://{s}/", .{ - protocol, - hostname, - }) - else - std.fmt.allocPrint(bun.default_allocator, "{s}://{s}:{d}/", .{ protocol, hostname, port })) catch unreachable; - } - - if (!strings.isAllASCII(hostname)) { - bun.default_allocator.free(@constCast(args.base_uri)); - args.base_uri = ""; - return global.throwInvalidArguments("Unicode hostnames must already be encoded for now.\nnew URL(input).hostname should do the trick.", .{}); - } - - args.base_url = URL.parse(args.base_uri); - } - - // I don't think there's a case where this can happen - // but let's check anyway, just in case - if (args.base_url.hostname.len == 0) { - bun.default_allocator.free(@constCast(args.base_uri)); - args.base_uri = ""; - return global.throwInvalidArguments("baseURI must have a hostname", .{}); - } - - if (args.base_url.username.len > 0 or args.base_url.password.len > 0) { - bun.default_allocator.free(@constCast(args.base_uri)); - args.base_uri = ""; - return global.throwInvalidArguments("baseURI can't have a username or password", .{}); - } - - return; - } -}; - -pub const HTTPStatusText = struct { - pub fn get(code: u16) ?[]const u8 { - return switch (code) { - 100 => "100 Continue", - 101 => "101 Switching protocols", - 102 => "102 Processing", - 103 => "103 Early Hints", - 200 => "200 OK", - 201 => "201 Created", - 202 => "202 Accepted", - 203 => "203 Non-Authoritative Information", - 204 => "204 No Content", - 205 => "205 Reset Content", - 206 => "206 Partial Content", - 207 => "207 Multi-Status", - 208 => "208 Already Reported", - 226 => "226 IM Used", - 300 => "300 Multiple Choices", - 301 => "301 Moved Permanently", - 302 => "302 Found", - 303 => "303 See Other", - 304 => "304 Not Modified", - 305 => "305 Use Proxy", - 306 => "306 Switch Proxy", - 307 => "307 Temporary Redirect", - 308 => "308 Permanent Redirect", - 400 => "400 Bad Request", - 401 => "401 Unauthorized", - 402 => "402 Payment Required", - 403 => "403 Forbidden", - 404 => "404 Not Found", - 405 => "405 Method Not Allowed", - 406 => "406 Not Acceptable", - 407 => "407 Proxy Authentication Required", - 408 => "408 Request Timeout", - 409 => "409 Conflict", - 410 => "410 Gone", - 411 => "411 Length Required", - 412 => "412 Precondition Failed", - 413 => "413 Payload Too Large", - 414 => "414 URI Too Long", - 415 => "415 Unsupported Media Type", - 416 => "416 Range Not Satisfiable", - 417 => "417 Expectation Failed", - 418 => "418 I'm a Teapot", - 421 => "421 Misdirected Request", - 422 => "422 Unprocessable Entity", - 423 => "423 Locked", - 424 => "424 Failed Dependency", - 425 => "425 Too Early", - 426 => "426 Upgrade Required", - 428 => "428 Precondition Required", - 429 => "429 Too Many Requests", - 431 => "431 Request Header Fields Too Large", - 451 => "451 Unavailable For Legal Reasons", - 500 => "500 Internal Server Error", - 501 => "501 Not Implemented", - 502 => "502 Bad Gateway", - 503 => "503 Service Unavailable", - 504 => "504 Gateway Timeout", - 505 => "505 HTTP Version Not Supported", - 506 => "506 Variant Also Negotiates", - 507 => "507 Insufficient Storage", - 508 => "508 Loop Detected", - 510 => "510 Not Extended", - 511 => "511 Network Authentication Required", - else => null, - }; - } -}; - -fn NewFlags(comptime debug_mode: bool) type { - return packed struct(u16) { - has_marked_complete: bool = false, - has_marked_pending: bool = false, - has_abort_handler: bool = false, - has_timeout_handler: bool = false, - has_sendfile_ctx: bool = false, - has_called_error_handler: bool = false, - needs_content_length: bool = false, - needs_content_range: bool = false, - /// Used to avoid looking at the uws.Request struct after it's been freed - is_transfer_encoding: bool = false, - - /// Used to identify if request can be safely deinitialized - is_waiting_for_request_body: bool = false, - /// Used in renderMissing in debug mode to show the user an HTML page - /// Used to avoid looking at the uws.Request struct after it's been freed - is_web_browser_navigation: if (debug_mode) bool else void = if (debug_mode) false, - has_written_status: bool = false, - response_protected: bool = false, - aborted: bool = false, - has_finalized: bun.DebugOnly(bool) = if (Environment.isDebug) false, - - is_error_promise_pending: bool = false, - - _padding: PaddingInt = 0, - - const PaddingInt = brk: { - var size: usize = 2; - if (Environment.isDebug) { - size -= 1; - } - - if (debug_mode) { - size -= 1; - } - - break :brk std.meta.Int(.unsigned, size); - }; - }; -} - -/// A generic wrapper for the HTTP(s) Server`RequestContext`s. -/// Only really exists because of `NewServer()` and `NewRequestContext()` generics. -pub const AnyRequestContext = struct { - pub const Pointer = bun.TaggedPointerUnion(.{ - HTTPServer.RequestContext, - HTTPSServer.RequestContext, - DebugHTTPServer.RequestContext, - DebugHTTPSServer.RequestContext, - }); - - tagged_pointer: Pointer, - - pub const Null: @This() = .{ .tagged_pointer = Pointer.Null }; - - pub fn init(request_ctx: anytype) AnyRequestContext { - return .{ .tagged_pointer = Pointer.init(request_ctx) }; - } - - pub fn memoryCost(self: AnyRequestContext) usize { - if (self.tagged_pointer.isNull()) { - return 0; - } - - switch (self.tagged_pointer.tag()) { - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { - return self.tagged_pointer.as(HTTPServer.RequestContext).memoryCost(); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { - return self.tagged_pointer.as(HTTPSServer.RequestContext).memoryCost(); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { - return self.tagged_pointer.as(DebugHTTPServer.RequestContext).memoryCost(); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { - return self.tagged_pointer.as(DebugHTTPSServer.RequestContext).memoryCost(); - }, - else => @panic("Unexpected AnyRequestContext tag"), - } - } - - pub fn get(self: AnyRequestContext, comptime T: type) ?*T { - return self.tagged_pointer.get(T); - } - - pub fn setTimeout(self: AnyRequestContext, seconds: c_uint) bool { - if (self.tagged_pointer.isNull()) { - return false; - } - - switch (self.tagged_pointer.tag()) { - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { - return self.tagged_pointer.as(HTTPServer.RequestContext).setTimeout(seconds); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { - return self.tagged_pointer.as(HTTPSServer.RequestContext).setTimeout(seconds); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { - return self.tagged_pointer.as(DebugHTTPServer.RequestContext).setTimeout(seconds); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { - return self.tagged_pointer.as(DebugHTTPSServer.RequestContext).setTimeout(seconds); - }, - else => @panic("Unexpected AnyRequestContext tag"), - } - return false; - } - - pub fn setCookies(self: AnyRequestContext, cookie_map: ?*JSC.WebCore.CookieMap) void { - if (self.tagged_pointer.isNull()) { - return; - } - - switch (self.tagged_pointer.tag()) { - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { - return self.tagged_pointer.as(HTTPServer.RequestContext).setCookies(cookie_map); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { - return self.tagged_pointer.as(HTTPSServer.RequestContext).setCookies(cookie_map); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { - return self.tagged_pointer.as(DebugHTTPServer.RequestContext).setCookies(cookie_map); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { - return self.tagged_pointer.as(DebugHTTPSServer.RequestContext).setCookies(cookie_map); - }, - else => @panic("Unexpected AnyRequestContext tag"), - } - } - - pub fn enableTimeoutEvents(self: AnyRequestContext) void { - if (self.tagged_pointer.isNull()) { - return; - } - - switch (self.tagged_pointer.tag()) { - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { - return self.tagged_pointer.as(HTTPServer.RequestContext).setTimeoutHandler(); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { - return self.tagged_pointer.as(HTTPSServer.RequestContext).setTimeoutHandler(); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { - return self.tagged_pointer.as(DebugHTTPServer.RequestContext).setTimeoutHandler(); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { - return self.tagged_pointer.as(DebugHTTPSServer.RequestContext).setTimeoutHandler(); - }, - else => @panic("Unexpected AnyRequestContext tag"), - } - } - - pub fn getRemoteSocketInfo(self: AnyRequestContext) ?uws.SocketAddress { - if (self.tagged_pointer.isNull()) { - return null; - } - - switch (self.tagged_pointer.tag()) { - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { - return self.tagged_pointer.as(HTTPServer.RequestContext).getRemoteSocketInfo(); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { - return self.tagged_pointer.as(HTTPSServer.RequestContext).getRemoteSocketInfo(); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { - return self.tagged_pointer.as(DebugHTTPServer.RequestContext).getRemoteSocketInfo(); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { - return self.tagged_pointer.as(DebugHTTPSServer.RequestContext).getRemoteSocketInfo(); - }, - else => @panic("Unexpected AnyRequestContext tag"), - } - } - - pub fn detachRequest(self: AnyRequestContext) void { - if (self.tagged_pointer.isNull()) { - return; - } - switch (self.tagged_pointer.tag()) { - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { - self.tagged_pointer.as(HTTPServer.RequestContext).req = null; - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { - self.tagged_pointer.as(HTTPSServer.RequestContext).req = null; - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { - self.tagged_pointer.as(DebugHTTPServer.RequestContext).req = null; - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { - self.tagged_pointer.as(DebugHTTPSServer.RequestContext).req = null; - }, - else => @panic("Unexpected AnyRequestContext tag"), - } - } - - /// Wont actually set anything if `self` is `.none` - pub fn setRequest(self: AnyRequestContext, req: *uws.Request) void { - if (self.tagged_pointer.isNull()) { - return; - } - - switch (self.tagged_pointer.tag()) { - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { - self.tagged_pointer.as(HTTPServer.RequestContext).req = req; - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { - self.tagged_pointer.as(HTTPSServer.RequestContext).req = req; - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { - self.tagged_pointer.as(DebugHTTPServer.RequestContext).req = req; - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { - self.tagged_pointer.as(DebugHTTPSServer.RequestContext).req = req; - }, - else => @panic("Unexpected AnyRequestContext tag"), - } - } - - pub fn getRequest(self: AnyRequestContext) ?*uws.Request { - if (self.tagged_pointer.isNull()) { - return null; - } - - switch (self.tagged_pointer.tag()) { - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { - return self.tagged_pointer.as(HTTPServer.RequestContext).req; - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { - return self.tagged_pointer.as(HTTPSServer.RequestContext).req; - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { - return self.tagged_pointer.as(DebugHTTPServer.RequestContext).req; - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { - return self.tagged_pointer.as(DebugHTTPSServer.RequestContext).req; - }, - else => @panic("Unexpected AnyRequestContext tag"), - } - } - - pub fn deref(self: AnyRequestContext) void { - if (self.tagged_pointer.isNull()) { - return; - } - - switch (self.tagged_pointer.tag()) { - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { - self.tagged_pointer.as(HTTPServer.RequestContext).deref(); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { - self.tagged_pointer.as(HTTPSServer.RequestContext).deref(); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { - self.tagged_pointer.as(DebugHTTPServer.RequestContext).deref(); - }, - @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { - self.tagged_pointer.as(DebugHTTPSServer.RequestContext).deref(); - }, - else => @panic("Unexpected AnyRequestContext tag"), - } - } -}; - -// This is defined separately partially to work-around an LLVM debugger bug. -fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comptime ThisServer: type) type { - return struct { - const RequestContext = @This(); - - const App = uws.NewApp(ssl_enabled); - pub threadlocal var pool: ?*RequestContext.RequestContextStackAllocator = null; - pub const ResponseStream = JSC.WebCore.HTTPServerWritable(ssl_enabled); - - // This pre-allocates up to 2,048 RequestContext structs. - // It costs about 655,632 bytes. - pub const RequestContextStackAllocator = bun.HiveArray(RequestContext, if (bun.heap_breakdown.enabled) 0 else 2048).Fallback; - - server: ?*ThisServer, - resp: ?*App.Response, - /// thread-local default heap allocator - /// this prevents an extra pthread_getspecific() call which shows up in profiling - allocator: std.mem.Allocator, - req: ?*uws.Request, - request_weakref: Request.WeakRef = .empty, - signal: ?*JSC.WebCore.AbortSignal = null, - method: HTTP.Method, - cookies: ?*JSC.WebCore.CookieMap = null, - - flags: NewFlags(debug_mode) = .{}, - - upgrade_context: ?*uws.SocketContext = null, - - /// We can only safely free once the request body promise is finalized - /// and the response is rejected - response_jsvalue: JSC.JSValue = JSC.JSValue.zero, - ref_count: u8 = 1, - - response_ptr: ?*JSC.WebCore.Response = null, - blob: JSC.WebCore.Blob.Any = JSC.WebCore.Blob.Any{ .Blob = .{} }, - - sendfile: SendfileContext = undefined, - - request_body_readable_stream_ref: JSC.WebCore.ReadableStream.Strong = .{}, - request_body: ?*WebCore.Body.Value.HiveRef = null, - request_body_buf: std.ArrayListUnmanaged(u8) = .{}, - request_body_content_len: usize = 0, - - sink: ?*ResponseStream.JSSink = null, - byte_stream: ?*JSC.WebCore.ByteStream = null, - // reference to the readable stream / byte_stream alive - readable_stream_ref: JSC.WebCore.ReadableStream.Strong = .{}, - - /// Used in errors - pathname: bun.String = bun.String.empty, - - /// Used either for temporary blob data or fallback - /// When the response body is a temporary value - response_buf_owned: std.ArrayListUnmanaged(u8) = .{}, - - /// Defer finalization until after the request handler task is completed? - defer_deinit_until_callback_completes: ?*bool = null, - - // TODO: support builtin compression - const can_sendfile = !ssl_enabled and !Environment.isWindows; - - pub fn memoryCost(this: *const RequestContext) usize { - // The Sink and ByteStream aren't owned by this. - return @sizeOf(RequestContext) + this.request_body_buf.capacity + this.response_buf_owned.capacity + this.blob.memoryCost(); - } - - pub inline fn isAsync(this: *const RequestContext) bool { - return this.defer_deinit_until_callback_completes == null; - } - - fn drainMicrotasks(this: *const RequestContext) void { - if (this.isAsync()) return; - if (this.server) |server| server.vm.drainMicrotasks(); - } - - pub fn setAbortHandler(this: *RequestContext) void { - if (this.flags.has_abort_handler) return; - if (this.resp) |resp| { - this.flags.has_abort_handler = true; - resp.onAborted(*RequestContext, RequestContext.onAbort, this); - } - } - - pub fn setCookies(this: *RequestContext, cookie_map: ?*JSC.WebCore.CookieMap) void { - if (this.cookies) |cookies| cookies.deref(); - this.cookies = cookie_map; - if (this.cookies) |cookies| cookies.ref(); - } - - pub fn setTimeoutHandler(this: *RequestContext) void { - if (this.flags.has_timeout_handler) return; - if (this.resp) |resp| { - this.flags.has_timeout_handler = true; - resp.onTimeout(*RequestContext, RequestContext.onTimeout, this); - } - } - - pub fn onResolve(_: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - ctxLog("onResolve", .{}); - - const arguments = callframe.arguments_old(2); - var ctx = arguments.ptr[1].asPromisePtr(@This()); - defer ctx.deref(); - - const result = arguments.ptr[0]; - result.ensureStillAlive(); - - handleResolve(ctx, result); - return JSValue.jsUndefined(); - } - - fn renderMissingInvalidResponse(ctx: *RequestContext, value: JSC.JSValue) void { - const class_name = value.getClassInfoName() orelse ""; - - if (ctx.server) |server| { - const globalThis: *JSC.JSGlobalObject = server.globalThis; - - Output.enableBuffering(); - var writer = Output.errorWriter(); - - if (bun.strings.eqlComptime(class_name, "Response")) { - Output.errGeneric("Expected a native Response object, but received a polyfilled Response object. Bun.serve() only supports native Response objects.", .{}); - } else if (value != .zero and !globalThis.hasException()) { - var formatter = JSC.ConsoleObject.Formatter{ - .globalThis = globalThis, - .quote_strings = true, - }; - defer formatter.deinit(); - Output.errGeneric("Expected a Response object, but received '{}'", .{value.toFmt(&formatter)}); - } else { - Output.errGeneric("Expected a Response object", .{}); - } - - Output.flush(); - if (!globalThis.hasException()) { - JSC.ConsoleObject.writeTrace(@TypeOf(&writer), &writer, globalThis); - } - Output.flush(); - } - ctx.renderMissing(); - } - - fn handleResolve(ctx: *RequestContext, value: JSC.JSValue) void { - if (ctx.isAbortedOrEnded() or ctx.didUpgradeWebSocket()) { - return; - } - - if (ctx.server == null) { - ctx.renderMissingInvalidResponse(value); - return; - } - if (value.isEmptyOrUndefinedOrNull() or !value.isCell()) { - ctx.renderMissingInvalidResponse(value); - return; - } - - const response = value.as(JSC.WebCore.Response) orelse { - ctx.renderMissingInvalidResponse(value); - return; - }; - ctx.response_jsvalue = value; - assert(!ctx.flags.response_protected); - ctx.flags.response_protected = true; - value.protect(); - - if (ctx.method == .HEAD) { - if (ctx.resp) |resp| { - var pair = HeaderResponsePair{ .this = ctx, .response = response }; - resp.runCorkedWithType(*HeaderResponsePair, doRenderHeadResponse, &pair); - } - return; - } - - ctx.render(response); - } - - pub fn shouldRenderMissing(this: *RequestContext) bool { - // If we did not respond yet, we should render missing - // To allow this all the conditions above should be true: - // 1 - still has a response (not detached) - // 2 - not aborted - // 3 - not marked completed - // 4 - not marked pending - // 5 - is the only reference of the context - // 6 - is not waiting for request body - // 7 - did not call sendfile - return this.resp != null and !this.flags.aborted and !this.flags.has_marked_complete and !this.flags.has_marked_pending and this.ref_count == 1 and !this.flags.is_waiting_for_request_body and !this.flags.has_sendfile_ctx; - } - - pub fn isDeadRequest(this: *RequestContext) bool { - // check if has pending promise or extra reference (aka not the only reference) - if (this.ref_count > 1) return false; - // check if the body is Locked (streaming) - if (this.request_body) |body| { - if (body.value == .Locked) { - return false; - } - } - - return true; - } - - /// destroy RequestContext, should be only called by deref or if defer_deinit_until_callback_completes is ref is set to true - fn deinit(this: *RequestContext) void { - this.detachResponse(); - this.endRequestStreamingAndDrain(); - // TODO: has_marked_complete is doing something? - this.flags.has_marked_complete = true; - - if (this.defer_deinit_until_callback_completes) |defer_deinit| { - defer_deinit.* = true; - ctxLog("deferred deinit ({*})", .{this}); - return; - } - - ctxLog("deinit ({*})", .{this}); - if (comptime Environment.isDebug) - assert(this.flags.has_finalized); - - this.request_body_buf.clearAndFree(this.allocator); - this.response_buf_owned.clearAndFree(this.allocator); - - if (this.request_body) |body| { - _ = body.unref(); - this.request_body = null; - } - - if (this.server) |server| { - this.server = null; - server.request_pool_allocator.put(this); - server.onRequestComplete(); - } - } - - pub fn deref(this: *RequestContext) void { - streamLog("deref", .{}); - assert(this.ref_count > 0); - const ref_count = this.ref_count; - this.ref_count -= 1; - if (ref_count == 1) { - this.finalizeWithoutDeinit(); - this.deinit(); - } - } - - pub fn ref(this: *RequestContext) void { - streamLog("ref", .{}); - this.ref_count += 1; - } - - pub fn onReject(_: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - ctxLog("onReject", .{}); - - const arguments = callframe.arguments_old(2); - const ctx = arguments.ptr[1].asPromisePtr(@This()); - const err = arguments.ptr[0]; - defer ctx.deref(); - handleReject(ctx, if (!err.isEmptyOrUndefinedOrNull()) err else .undefined); - return JSValue.jsUndefined(); - } - - fn handleReject(ctx: *RequestContext, value: JSC.JSValue) void { - if (ctx.isAbortedOrEnded()) { - return; - } - - const resp = ctx.resp.?; - const has_responded = resp.hasResponded(); - if (!has_responded) { - const original_state = ctx.defer_deinit_until_callback_completes; - var should_deinit_context = if (original_state) |defer_deinit| defer_deinit.* else false; - ctx.defer_deinit_until_callback_completes = &should_deinit_context; - ctx.runErrorHandler( - value, - ); - ctx.defer_deinit_until_callback_completes = original_state; - // we try to deinit inside runErrorHandler so we just return here and let it deinit - if (should_deinit_context) { - ctx.deinit(); - return; - } - } - // check again in case it get aborted after runErrorHandler - if (ctx.isAbortedOrEnded()) { - return; - } - - // I don't think this case happens? - if (ctx.didUpgradeWebSocket()) { - return; - } - - if (!resp.hasResponded() and !ctx.flags.has_marked_pending and !ctx.flags.is_error_promise_pending) { - ctx.renderMissing(); - return; - } - } - - pub fn renderMissing(ctx: *RequestContext) void { - if (ctx.resp) |resp| { - resp.runCorkedWithType(*RequestContext, renderMissingCorked, ctx); - } - } - - pub fn renderMissingCorked(ctx: *RequestContext) void { - if (ctx.resp) |resp| { - if (comptime !debug_mode) { - if (!ctx.flags.has_written_status) - resp.writeStatus("204 No Content"); - ctx.flags.has_written_status = true; - ctx.end("", ctx.shouldCloseConnection()); - return; - } - // avoid writing the status again and mismatching the content-length - if (ctx.flags.has_written_status) { - ctx.end("", ctx.shouldCloseConnection()); - return; - } - - if (ctx.flags.is_web_browser_navigation) { - resp.writeStatus("200 OK"); - ctx.flags.has_written_status = true; - - resp.writeHeader("content-type", MimeType.html.value); - resp.writeHeader("content-encoding", "gzip"); - resp.writeHeaderInt("content-length", welcome_page_html_gz.len); - ctx.end(welcome_page_html_gz, ctx.shouldCloseConnection()); - return; - } - const missing_content = "Welcome to Bun! To get started, return a Response object."; - resp.writeStatus("200 OK"); - resp.writeHeader("content-type", MimeType.text.value); - resp.writeHeaderInt("content-length", missing_content.len); - ctx.flags.has_written_status = true; - ctx.end(missing_content, ctx.shouldCloseConnection()); - } - } - - pub fn renderDefaultError( - this: *RequestContext, - log: *logger.Log, - err: anyerror, - exceptions: []Api.JsException, - comptime fmt: string, - args: anytype, - ) void { - if (!this.flags.has_written_status) { - this.flags.has_written_status = true; - if (this.resp) |resp| { - resp.writeStatus("500 Internal Server Error"); - resp.writeHeader("content-type", MimeType.html.value); - } - } - - const allocator = this.allocator; - - const fallback_container = allocator.create(Api.FallbackMessageContainer) catch unreachable; - defer allocator.destroy(fallback_container); - fallback_container.* = Api.FallbackMessageContainer{ - .message = std.fmt.allocPrint(allocator, comptime Output.prettyFmt(fmt, false), args) catch unreachable, - .router = null, - .reason = .fetch_event_handler, - .cwd = VirtualMachine.get().transpiler.fs.top_level_dir, - .problems = Api.Problems{ - .code = @as(u16, @truncate(@intFromError(err))), - .name = @errorName(err), - .exceptions = exceptions, - .build = log.toAPI(allocator) catch unreachable, - }, - }; - - if (comptime fmt.len > 0) Output.prettyErrorln(fmt, args); - Output.flush(); - - var bb = std.ArrayList(u8).init(allocator); - const bb_writer = bb.writer(); - - Fallback.renderBackend( - allocator, - fallback_container, - @TypeOf(bb_writer), - bb_writer, - ) catch unreachable; - if (this.resp == null or this.resp.?.tryEnd(bb.items, bb.items.len, this.shouldCloseConnection())) { - bb.clearAndFree(); - this.detachResponse(); - this.endRequestStreamingAndDrain(); - this.finalizeWithoutDeinit(); - this.deref(); - return; - } - - this.flags.has_marked_pending = true; - this.response_buf_owned = std.ArrayListUnmanaged(u8){ .items = bb.items, .capacity = bb.capacity }; - - if (this.resp) |resp| { - resp.onWritable(*RequestContext, onWritableCompleteResponseBuffer, this); - } - } - - pub fn renderResponseBuffer(this: *RequestContext) void { - if (this.resp) |resp| { - resp.onWritable(*RequestContext, onWritableResponseBuffer, this); - } - } - - /// Render a complete response buffer - pub fn renderResponseBufferAndMetadata(this: *RequestContext) void { - if (this.resp) |resp| { - this.renderMetadata(); - - if (!resp.tryEnd( - this.response_buf_owned.items, - this.response_buf_owned.items.len, - this.shouldCloseConnection(), - )) { - this.flags.has_marked_pending = true; - resp.onWritable(*RequestContext, onWritableCompleteResponseBuffer, this); - return; - } - } - this.detachResponse(); - this.endRequestStreamingAndDrain(); - this.deref(); - } - - /// Drain a partial response buffer - pub fn drainResponseBufferAndMetadata(this: *RequestContext) void { - if (this.resp) |resp| { - this.renderMetadata(); - - _ = resp.write( - this.response_buf_owned.items, - ); - } - this.response_buf_owned.items.len = 0; - } - - pub fn end(this: *RequestContext, data: []const u8, closeConnection: bool) void { - if (this.resp) |resp| { - defer this.deref(); - - this.detachResponse(); - this.endRequestStreamingAndDrain(); - resp.end(data, closeConnection); - } - } - - pub fn endStream(this: *RequestContext, closeConnection: bool) void { - ctxLog("endStream", .{}); - if (this.resp) |resp| { - defer this.deref(); - - this.detachResponse(); - this.endRequestStreamingAndDrain(); - // This will send a terminating 0\r\n\r\n chunk to the client - // We only want to do that if they're still expecting a body - // We cannot call this function if the Content-Length header was previously set - if (resp.state().isResponsePending()) - resp.endStream(closeConnection); - } - } - - pub fn endWithoutBody(this: *RequestContext, closeConnection: bool) void { - if (this.resp) |resp| { - defer this.deref(); - - this.detachResponse(); - this.endRequestStreamingAndDrain(); - resp.endWithoutBody(closeConnection); - } - } - - pub fn onWritableResponseBuffer(this: *RequestContext, _: u64, resp: *App.Response) bool { - ctxLog("onWritableResponseBuffer", .{}); - - assert(this.resp == resp); - if (this.isAbortedOrEnded()) { - return false; - } - this.end("", this.shouldCloseConnection()); - return false; - } - - // TODO: should we cork? - pub fn onWritableCompleteResponseBufferAndMetadata(this: *RequestContext, write_offset: u64, resp: *App.Response) bool { - ctxLog("onWritableCompleteResponseBufferAndMetadata", .{}); - assert(this.resp == resp); - - if (this.isAbortedOrEnded()) { - return false; - } - - if (!this.flags.has_written_status) { - this.renderMetadata(); - } - - if (this.method == .HEAD) { - this.endWithoutBody(this.shouldCloseConnection()); - return false; - } - - return this.sendWritableBytesForCompleteResponseBuffer(this.response_buf_owned.items, write_offset, resp); - } - - pub fn onWritableCompleteResponseBuffer(this: *RequestContext, write_offset: u64, resp: *App.Response) bool { - ctxLog("onWritableCompleteResponseBuffer", .{}); - assert(this.resp == resp); - if (this.isAbortedOrEnded()) { - return false; - } - return this.sendWritableBytesForCompleteResponseBuffer(this.response_buf_owned.items, write_offset, resp); - } - - pub fn create(this: *RequestContext, server: *ThisServer, req: *uws.Request, resp: *App.Response, should_deinit_context: ?*bool, method: ?bun.http.Method) void { - this.* = .{ - .allocator = server.allocator, - .resp = resp, - .req = req, - .method = method orelse HTTP.Method.which(req.method()) orelse .GET, - .server = server, - .defer_deinit_until_callback_completes = should_deinit_context, - }; - - ctxLog("create ({*})", .{this}); - } - - pub fn onTimeout(this: *RequestContext, resp: *App.Response) void { - assert(this.resp == resp); - assert(this.server != null); - - var any_js_calls = false; - var vm = this.server.?.vm; - const globalThis = this.server.?.globalThis; - defer { - // This is a task in the event loop. - // If we called into JavaScript, we must drain the microtask queue - if (any_js_calls) { - vm.drainMicrotasks(); - } - } - - if (this.request_weakref.get()) |request| { - if (request.internal_event_callback.trigger(Request.InternalJSEventCallback.EventType.timeout, globalThis)) { - any_js_calls = true; - } - } - } - - pub fn onAbort(this: *RequestContext, resp: *App.Response) void { - assert(this.resp == resp); - assert(!this.flags.aborted); - assert(this.server != null); - // mark request as aborted - this.flags.aborted = true; - - this.detachResponse(); - var any_js_calls = false; - var vm = this.server.?.vm; - const globalThis = this.server.?.globalThis; - defer { - // This is a task in the event loop. - // If we called into JavaScript, we must drain the microtask queue - if (any_js_calls) { - vm.drainMicrotasks(); - } - this.deref(); - } - - if (this.request_weakref.get()) |request| { - request.request_context = AnyRequestContext.Null; - if (request.internal_event_callback.trigger(Request.InternalJSEventCallback.EventType.abort, globalThis)) { - any_js_calls = true; - } - // we can already clean this strong refs - request.internal_event_callback.deinit(); - this.request_weakref.deref(); - } - // if signal is not aborted, abort the signal - if (this.signal) |signal| { - this.signal = null; - defer { - signal.pendingActivityUnref(); - signal.unref(); - } - if (!signal.aborted()) { - signal.signal(globalThis, .ConnectionClosed); - any_js_calls = true; - } - } - - //if have sink, call onAborted on sink - if (this.sink) |wrapper| { - wrapper.sink.abort(); - return; - } - - // if we can, free the request now. - if (this.isDeadRequest()) { - this.finalizeWithoutDeinit(); - } else { - if (this.endRequestStreaming()) { - any_js_calls = true; - } - - if (this.response_ptr) |response| { - if (response.body.value == .Locked) { - var strong_readable = response.body.value.Locked.readable; - response.body.value.Locked.readable = .{}; - defer strong_readable.deinit(); - if (strong_readable.get(globalThis)) |readable| { - readable.abort(globalThis); - any_js_calls = true; - } - } - } - } - } - - // This function may be called multiple times - // so it's important that we can safely do that - pub fn finalizeWithoutDeinit(this: *RequestContext) void { - ctxLog("finalizeWithoutDeinit ({*})", .{this}); - this.blob.detach(); - assert(this.server != null); - const globalThis = this.server.?.globalThis; - - if (comptime Environment.isDebug) { - ctxLog("finalizeWithoutDeinit: has_finalized {any}", .{this.flags.has_finalized}); - this.flags.has_finalized = true; - } - - if (this.response_jsvalue != .zero) { - ctxLog("finalizeWithoutDeinit: response_jsvalue != .zero", .{}); - if (this.flags.response_protected) { - this.response_jsvalue.unprotect(); - this.flags.response_protected = false; - } - this.response_jsvalue = JSC.JSValue.zero; - } - - this.request_body_readable_stream_ref.deinit(); - - if (this.cookies) |cookies| { - this.cookies = null; - cookies.deref(); - } - - if (this.request_weakref.get()) |request| { - request.request_context = AnyRequestContext.Null; - // we can already clean this strong refs - request.internal_event_callback.deinit(); - this.request_weakref.deref(); - } - - // if signal is not aborted, abort the signal - if (this.signal) |signal| { - this.signal = null; - defer { - signal.pendingActivityUnref(); - signal.unref(); - } - if (this.flags.aborted and !signal.aborted()) { - signal.signal(globalThis, .ConnectionClosed); - } - } - - // Case 1: - // User called .blob(), .json(), text(), or .arrayBuffer() on the Request object - // but we received nothing or the connection was aborted - // the promise is pending - // Case 2: - // User ignored the body and the connection was aborted or ended - // Case 3: - // Stream was not consumed and the connection was aborted or ended - _ = this.endRequestStreaming(); - - if (this.byte_stream) |stream| { - ctxLog("finalizeWithoutDeinit: stream != null", .{}); - - this.byte_stream = null; - stream.unpipeWithoutDeref(); - } - - this.readable_stream_ref.deinit(); - - if (!this.pathname.isEmpty()) { - this.pathname.deref(); - this.pathname = bun.String.empty; - } - } - - pub fn endSendFile(this: *RequestContext, writeOffSet: usize, closeConnection: bool) void { - if (this.resp) |resp| { - defer this.deref(); - - this.detachResponse(); - this.endRequestStreamingAndDrain(); - resp.endSendFile(writeOffSet, closeConnection); - } - } - - fn cleanupAndFinalizeAfterSendfile(this: *RequestContext) void { - const sendfile = this.sendfile; - this.endSendFile(sendfile.offset, this.shouldCloseConnection()); - - // use node syscall so that we don't segfault on BADF - if (sendfile.auto_close) - sendfile.fd.close(); - } - const separator: string = "\r\n"; - const separator_iovec = [1]std.posix.iovec_const{.{ - .iov_base = separator.ptr, - .iov_len = separator.len, - }}; - - pub fn onSendfile(this: *RequestContext) bool { - if (this.isAbortedOrEnded()) { - this.cleanupAndFinalizeAfterSendfile(); - return false; - } - const resp = this.resp.?; - - const adjusted_count_temporary = @min(@as(u64, this.sendfile.remain), @as(u63, std.math.maxInt(u63))); - // TODO we should not need this int cast; improve the return type of `@min` - const adjusted_count = @as(u63, @intCast(adjusted_count_temporary)); - - if (Environment.isLinux) { - var signed_offset = @as(i64, @intCast(this.sendfile.offset)); - const start = this.sendfile.offset; - const val = linux.sendfile(this.sendfile.socket_fd.cast(), this.sendfile.fd.cast(), &signed_offset, this.sendfile.remain); - this.sendfile.offset = @as(Blob.SizeType, @intCast(signed_offset)); - - const errcode = bun.sys.getErrno(val); - - this.sendfile.remain -|= @as(Blob.SizeType, @intCast(this.sendfile.offset -| start)); - - if (errcode != .SUCCESS or this.isAbortedOrEnded() or this.sendfile.remain == 0 or val == 0) { - if (errcode != .AGAIN and errcode != .SUCCESS and errcode != .PIPE and errcode != .NOTCONN) { - Output.prettyErrorln("Error: {s}", .{@tagName(errcode)}); - Output.flush(); - } - this.cleanupAndFinalizeAfterSendfile(); - return errcode != .SUCCESS; - } - } else { - var sbytes: std.posix.off_t = adjusted_count; - const signed_offset = @as(i64, @bitCast(@as(u64, this.sendfile.offset))); - const errcode = bun.sys.getErrno(std.c.sendfile( - this.sendfile.fd.cast(), - this.sendfile.socket_fd.cast(), - signed_offset, - &sbytes, - null, - 0, - )); - const wrote = @as(Blob.SizeType, @intCast(sbytes)); - this.sendfile.offset +|= wrote; - this.sendfile.remain -|= wrote; - if (errcode != .AGAIN or this.isAbortedOrEnded() or this.sendfile.remain == 0 or sbytes == 0) { - if (errcode != .AGAIN and errcode != .SUCCESS and errcode != .PIPE and errcode != .NOTCONN) { - Output.prettyErrorln("Error: {s}", .{@tagName(errcode)}); - Output.flush(); - } - this.cleanupAndFinalizeAfterSendfile(); - return errcode == .SUCCESS; - } - } - - if (!this.sendfile.has_set_on_writable) { - this.sendfile.has_set_on_writable = true; - this.flags.has_marked_pending = true; - resp.onWritable(*RequestContext, onWritableSendfile, this); - } - - resp.markNeedsMore(); - - return true; - } - - pub fn onWritableBytes(this: *RequestContext, write_offset: u64, resp: *App.Response) bool { - ctxLog("onWritableBytes", .{}); - assert(this.resp == resp); - if (this.isAbortedOrEnded()) { - return false; - } - - // Copy to stack memory to prevent aliasing issues in release builds - const blob = this.blob; - const bytes = blob.slice(); - - _ = this.sendWritableBytesForBlob(bytes, write_offset, resp); - return true; - } - - pub fn sendWritableBytesForBlob(this: *RequestContext, bytes_: []const u8, write_offset_: u64, resp: *App.Response) bool { - assert(this.resp == resp); - const write_offset: usize = write_offset_; - - const bytes = bytes_[@min(bytes_.len, @as(usize, @truncate(write_offset)))..]; - if (resp.tryEnd(bytes, bytes_.len, this.shouldCloseConnection())) { - this.detachResponse(); - this.endRequestStreamingAndDrain(); - this.deref(); - return true; - } else { - this.flags.has_marked_pending = true; - resp.onWritable(*RequestContext, onWritableBytes, this); - return true; - } - } - - pub fn sendWritableBytesForCompleteResponseBuffer(this: *RequestContext, bytes_: []const u8, write_offset_: u64, resp: *App.Response) bool { - const write_offset: usize = write_offset_; - assert(this.resp == resp); - - const bytes = bytes_[@min(bytes_.len, @as(usize, @truncate(write_offset)))..]; - if (resp.tryEnd(bytes, bytes_.len, this.shouldCloseConnection())) { - this.response_buf_owned.items.len = 0; - this.detachResponse(); - this.endRequestStreamingAndDrain(); - this.deref(); - } else { - this.flags.has_marked_pending = true; - resp.onWritable(*RequestContext, onWritableCompleteResponseBuffer, this); - } - - return true; - } - - pub fn onWritableSendfile(this: *RequestContext, _: u64, _: *App.Response) bool { - ctxLog("onWritableSendfile", .{}); - return this.onSendfile(); - } - - // We tried open() in another thread for this - // it was not faster due to the mountain of syscalls - pub fn renderSendFile(this: *RequestContext, blob: JSC.WebCore.Blob) void { - if (this.resp == null or this.server == null) return; - const globalThis = this.server.?.globalThis; - const resp = this.resp.?; - - this.blob = .{ .Blob = blob }; - const file = &this.blob.store().?.data.file; - var file_buf: bun.PathBuffer = undefined; - const auto_close = file.pathlike != .fd; - const fd = if (!auto_close) - file.pathlike.fd - else switch (bun.sys.open(file.pathlike.path.sliceZ(&file_buf), bun.O.RDONLY | bun.O.NONBLOCK | bun.O.CLOEXEC, 0)) { - .result => |_fd| _fd, - .err => |err| return this.runErrorHandler(err.withPath(file.pathlike.path.slice()).toJSC(globalThis)), - }; - - // stat only blocks if the target is a file descriptor - const stat: bun.Stat = switch (bun.sys.fstat(fd)) { - .result => |result| result, - .err => |err| { - this.runErrorHandler(err.withPathLike(file.pathlike).toJSC(globalThis)); - if (auto_close) { - fd.close(); - } - return; - }, - }; - - if (Environment.isMac) { - if (!bun.isRegularFile(stat.mode)) { - if (auto_close) { - fd.close(); - } - - var err = bun.sys.Error{ - .errno = @as(bun.sys.Error.Int, @intCast(@intFromEnum(std.posix.E.INVAL))), - .syscall = .sendfile, - }; - var sys = err.withPathLike(file.pathlike).toSystemError(); - sys.message = bun.String.static("MacOS does not support sending non-regular files"); - this.runErrorHandler(sys.toErrorInstance( - globalThis, - )); - return; - } - } - - if (Environment.isLinux) { - if (!(bun.isRegularFile(stat.mode) or std.posix.S.ISFIFO(stat.mode) or std.posix.S.ISSOCK(stat.mode))) { - if (auto_close) { - fd.close(); - } - - var err = bun.sys.Error{ - .errno = @as(bun.sys.Error.Int, @intCast(@intFromEnum(std.posix.E.INVAL))), - .syscall = .sendfile, - }; - var sys = err.withPathLike(file.pathlike).toShellSystemError(); - sys.message = bun.String.static("File must be regular or FIFO"); - this.runErrorHandler(sys.toErrorInstance(globalThis)); - return; - } - } - - const original_size = this.blob.Blob.size; - const stat_size = @as(Blob.SizeType, @intCast(stat.size)); - this.blob.Blob.size = if (bun.isRegularFile(stat.mode)) - stat_size - else - @min(original_size, stat_size); - - this.flags.needs_content_length = true; - - this.sendfile = .{ - .fd = fd, - .remain = this.blob.Blob.offset + original_size, - .offset = this.blob.Blob.offset, - .auto_close = auto_close, - .socket_fd = if (!this.isAbortedOrEnded()) resp.getNativeHandle() else bun.invalid_fd, - }; - - // if we are sending only part of a file, include the content-range header - // only include content-range automatically when using a file path instead of an fd - // this is to better support manually controlling the behavior - if (bun.isRegularFile(stat.mode) and auto_close) { - this.flags.needs_content_range = (this.sendfile.remain -| this.sendfile.offset) != stat_size; - } - - // we know the bounds when we are sending a regular file - if (bun.isRegularFile(stat.mode)) { - this.sendfile.offset = @min(this.sendfile.offset, stat_size); - this.sendfile.remain = @min(@max(this.sendfile.remain, this.sendfile.offset), stat_size) -| this.sendfile.offset; - } - - resp.runCorkedWithType(*RequestContext, renderMetadataAndNewline, this); - - if (this.sendfile.remain == 0 or !this.method.hasBody()) { - this.cleanupAndFinalizeAfterSendfile(); - return; - } - - _ = this.onSendfile(); - } - - pub fn renderMetadataAndNewline(this: *RequestContext) void { - if (this.resp) |resp| { - this.renderMetadata(); - resp.prepareForSendfile(); - } - } - - pub fn doSendfile(this: *RequestContext, blob: Blob) void { - if (this.isAbortedOrEnded()) { - return; - } - - if (this.flags.has_sendfile_ctx) return; - - this.flags.has_sendfile_ctx = true; - - if (comptime can_sendfile) { - return this.renderSendFile(blob); - } - if (this.server) |server| { - this.ref(); - this.blob.Blob.doReadFileInternal(*RequestContext, this, onReadFile, server.globalThis); - } - } - - pub fn onReadFile(this: *RequestContext, result: Blob.read_file.ReadFileResultType) void { - defer this.deref(); - - if (this.isAbortedOrEnded()) { - return; - } - - if (result == .err) { - if (this.server) |server| { - this.runErrorHandler(result.err.toErrorInstance(server.globalThis)); - } - return; - } - - const is_temporary = result.result.is_temporary; - - if (comptime Environment.allow_assert) { - assert(this.blob == .Blob); - } - - if (!is_temporary) { - this.blob.Blob.resolveSize(); - this.doRenderBlob(); - } else { - const stat_size = @as(Blob.SizeType, @intCast(result.result.total_size)); - - if (this.blob == .Blob) { - const original_size = this.blob.Blob.size; - // if we dont know the size we use the stat size - this.blob.Blob.size = if (original_size == 0 or original_size == Blob.max_size) - stat_size - else // the blob can be a slice of a file - @max(original_size, stat_size); - } - - if (!this.flags.has_written_status) - this.flags.needs_content_range = true; - - // this is used by content-range - this.sendfile = .{ - .fd = bun.invalid_fd, - .remain = @as(Blob.SizeType, @truncate(result.result.buf.len)), - .offset = if (this.blob == .Blob) this.blob.Blob.offset else 0, - .auto_close = false, - .socket_fd = bun.invalid_fd, - }; - - this.response_buf_owned = .{ .items = result.result.buf, .capacity = result.result.buf.len }; - this.resp.?.runCorkedWithType(*RequestContext, renderResponseBufferAndMetadata, this); - } - } - - pub fn doRenderWithBodyLocked(this: *anyopaque, value: *JSC.WebCore.Body.Value) void { - doRenderWithBody(bun.cast(*RequestContext, this), value); - } - - fn renderWithBlobFromBodyValue(this: *RequestContext) void { - if (this.isAbortedOrEnded()) { - return; - } - - if (this.blob.needsToReadFile()) { - if (!this.flags.has_sendfile_ctx) - this.doSendfile(this.blob.Blob); - return; - } - - this.doRenderBlob(); - } - - const StreamPair = struct { this: *RequestContext, stream: JSC.WebCore.ReadableStream }; - - fn handleFirstStreamWrite(this: *@This()) void { - if (!this.flags.has_written_status) { - this.renderMetadata(); - } - } - - fn doRenderStream(pair: *StreamPair) void { - ctxLog("doRenderStream", .{}); - var this = pair.this; - var stream = pair.stream; - assert(this.server != null); - const globalThis = this.server.?.globalThis; - - if (this.isAbortedOrEnded()) { - stream.cancel(globalThis); - this.readable_stream_ref.deinit(); - return; - } - const resp = this.resp.?; - - stream.value.ensureStillAlive(); - - var response_stream = this.allocator.create(ResponseStream.JSSink) catch unreachable; - response_stream.* = ResponseStream.JSSink{ - .sink = .{ - .res = resp, - .allocator = this.allocator, - .buffer = bun.ByteList{}, - .onFirstWrite = @ptrCast(&handleFirstStreamWrite), - .ctx = this, - .globalThis = globalThis, - }, - }; - var signal = &response_stream.sink.signal; - this.sink = response_stream; - - signal.* = ResponseStream.JSSink.SinkSignal.init(JSValue.zero); - - // explicitly set it to a dead pointer - // we use this memory address to disable signals being sent - signal.clear(); - assert(signal.isDead()); - // we need to render metadata before assignToStream because the stream can call res.end - // and this would auto write an 200 status - if (!this.flags.has_written_status) { - this.renderMetadata(); - } - - // We are already corked! - const assignment_result: JSValue = ResponseStream.JSSink.assignToStream( - globalThis, - stream.value, - response_stream, - @as(**anyopaque, @ptrCast(&signal.ptr)), - ); - - assignment_result.ensureStillAlive(); - - // assert that it was updated - assert(!signal.isDead()); - - if (comptime Environment.allow_assert) { - if (resp.hasResponded()) { - streamLog("responded", .{}); - } - } - - this.flags.aborted = this.flags.aborted or response_stream.sink.aborted; - - if (assignment_result.toError()) |err_value| { - streamLog("returned an error", .{}); - response_stream.detach(); - this.sink = null; - response_stream.sink.destroy(); - return this.handleReject(err_value); - } - - if (resp.hasResponded()) { - streamLog("done", .{}); - response_stream.detach(); - this.sink = null; - response_stream.sink.destroy(); - stream.done(globalThis); - this.readable_stream_ref.deinit(); - this.endStream(this.shouldCloseConnection()); - return; - } - - if (!assignment_result.isEmptyOrUndefinedOrNull()) { - assignment_result.ensureStillAlive(); - // it returns a Promise when it goes through ReadableStreamDefaultReader - if (assignment_result.asAnyPromise()) |promise| { - streamLog("returned a promise", .{}); - this.drainMicrotasks(); - - switch (promise.status(globalThis.vm())) { - .pending => { - streamLog("promise still Pending", .{}); - if (!this.flags.has_written_status) { - response_stream.sink.onFirstWrite = null; - response_stream.sink.ctx = null; - this.renderMetadata(); - } - - // TODO: should this timeout? - this.response_ptr.?.body.value = .{ - .Locked = .{ - .readable = JSC.WebCore.ReadableStream.Strong.init(stream, globalThis), - .global = globalThis, - }, - }; - this.ref(); - assignment_result.then( - globalThis, - this, - onResolveStream, - onRejectStream, - ); - // the response_stream should be GC'd - - }, - .fulfilled => { - streamLog("promise Fulfilled", .{}); - var readable_stream_ref = this.readable_stream_ref; - this.readable_stream_ref = .{}; - defer { - stream.done(globalThis); - readable_stream_ref.deinit(); - } - - this.handleResolveStream(); - }, - .rejected => { - streamLog("promise Rejected", .{}); - var readable_stream_ref = this.readable_stream_ref; - this.readable_stream_ref = .{}; - defer { - stream.cancel(globalThis); - readable_stream_ref.deinit(); - } - this.handleRejectStream(globalThis, promise.result(globalThis.vm())); - }, - } - return; - } else { - // if is not a promise we treat it as Error - streamLog("returned an error", .{}); - response_stream.detach(); - this.sink = null; - response_stream.sink.destroy(); - return this.handleReject(assignment_result); - } - } - - if (this.isAbortedOrEnded()) { - response_stream.detach(); - stream.cancel(globalThis); - defer this.readable_stream_ref.deinit(); - - response_stream.sink.markDone(); - response_stream.sink.onFirstWrite = null; - - response_stream.sink.finalize(); - return; - } - var readable_stream_ref = this.readable_stream_ref; - this.readable_stream_ref = .{}; - defer readable_stream_ref.deinit(); - - const is_in_progress = response_stream.sink.has_backpressure or !(response_stream.sink.wrote == 0 and - response_stream.sink.buffer.len == 0); - - if (!stream.isLocked(globalThis) and !is_in_progress) { - if (JSC.WebCore.ReadableStream.fromJS(stream.value, globalThis)) |comparator| { - if (std.meta.activeTag(comparator.ptr) == std.meta.activeTag(stream.ptr)) { - streamLog("is not locked", .{}); - this.renderMissing(); - return; - } - } - } - - streamLog("is in progress, but did not return a Promise. Finalizing request context", .{}); - response_stream.sink.onFirstWrite = null; - response_stream.sink.ctx = null; - response_stream.detach(); - stream.cancel(globalThis); - response_stream.sink.markDone(); - this.renderMissing(); - } - - const streamLog = Output.scoped(.ReadableStream, false); - - pub fn didUpgradeWebSocket(this: *RequestContext) bool { - return @intFromPtr(this.upgrade_context) == std.math.maxInt(usize); - } - - fn toAsyncWithoutAbortHandler(ctx: *RequestContext, req: *uws.Request, request_object: *Request) void { - request_object.request_context.setRequest(req); - assert(ctx.server != null); - - request_object.ensureURL() catch { - request_object.url = bun.String.empty; - }; - - // we have to clone the request headers here since they will soon belong to a different request - if (!request_object.hasFetchHeaders()) { - request_object.setFetchHeaders(.createFromUWS(req)); - } - - // This object dies after the stack frame is popped - // so we have to clear it in here too - request_object.request_context.detachRequest(); - } - - fn toAsync( - ctx: *RequestContext, - req: *uws.Request, - request_object: *Request, - ) void { - ctxLog("toAsync", .{}); - ctx.toAsyncWithoutAbortHandler(req, request_object); - if (comptime debug_mode) { - ctx.pathname = request_object.url.clone(); - } - ctx.setAbortHandler(); - } - - fn endRequestStreamingAndDrain(this: *RequestContext) void { - assert(this.server != null); - - if (this.endRequestStreaming()) { - this.server.?.vm.drainMicrotasks(); - } - } - fn endRequestStreaming(this: *RequestContext) bool { - assert(this.server != null); - - this.request_body_buf.clearAndFree(bun.default_allocator); - - // if we cannot, we have to reject pending promises - // first, we reject the request body promise - if (this.request_body) |body| { - // User called .blob(), .json(), text(), or .arrayBuffer() on the Request object - // but we received nothing or the connection was aborted - if (body.value == .Locked) { - body.value.toErrorInstance(.{ .AbortReason = .ConnectionClosed }, this.server.?.globalThis); - return true; - } - } - return false; - } - fn detachResponse(this: *RequestContext) void { - this.request_body_buf.clearAndFree(bun.default_allocator); - - if (this.resp) |resp| { - this.resp = null; - - if (this.flags.is_waiting_for_request_body) { - this.flags.is_waiting_for_request_body = false; - resp.clearOnData(); - } - if (this.flags.has_abort_handler) { - resp.clearAborted(); - this.flags.has_abort_handler = false; - } - if (this.flags.has_timeout_handler) { - resp.clearTimeout(); - this.flags.has_timeout_handler = false; - } - } - } - - fn isAbortedOrEnded(this: *const RequestContext) bool { - // resp == null or aborted or server.stop(true) - return this.resp == null or this.flags.aborted or this.server == null or this.server.?.flags.terminated; - } - const HeaderResponseSizePair = struct { this: *RequestContext, size: usize }; - pub fn doRenderHeadResponseAfterS3SizeResolved(pair: *HeaderResponseSizePair) void { - var this = pair.this; - this.renderMetadata(); - - if (this.resp) |resp| { - resp.writeHeaderInt("content-length", pair.size); - } - this.endWithoutBody(this.shouldCloseConnection()); - this.deref(); - } - pub fn onS3SizeResolved(result: S3.S3StatResult, this: *RequestContext) void { - defer { - this.deref(); - } - if (this.resp) |resp| { - var pair = HeaderResponseSizePair{ .this = this, .size = switch (result) { - .failure, .not_found => 0, - .success => |stat| stat.size, - } }; - resp.runCorkedWithType(*HeaderResponseSizePair, doRenderHeadResponseAfterS3SizeResolved, &pair); - } - } - const HeaderResponsePair = struct { this: *RequestContext, response: *JSC.WebCore.Response }; - - fn doRenderHeadResponse(pair: *HeaderResponsePair) void { - var this = pair.this; - var response = pair.response; - if (this.resp == null) { - return; - } - // we will render the content-length header later manually so we set this to false - this.flags.needs_content_length = false; - // Always this.renderMetadata() before sending the content-length or transfer-encoding header so status is sent first - - const resp = this.resp.?; - this.response_ptr = response; - const server = this.server orelse { - // server detached? - this.renderMetadata(); - resp.writeHeaderInt("content-length", 0); - this.endWithoutBody(this.shouldCloseConnection()); - return; - }; - const globalThis = server.globalThis; - if (response.getFetchHeaders()) |headers| { - // first respect the headers - if (headers.fastGet(.TransferEncoding)) |transfer_encoding| { - const transfer_encoding_str = transfer_encoding.toSlice(server.allocator); - defer transfer_encoding_str.deinit(); - this.renderMetadata(); - resp.writeHeader("transfer-encoding", transfer_encoding_str.slice()); - this.endWithoutBody(this.shouldCloseConnection()); - - return; - } - if (headers.fastGet(.ContentLength)) |content_length| { - const content_length_str = content_length.toSlice(server.allocator); - defer content_length_str.deinit(); - this.renderMetadata(); - - const len = std.fmt.parseInt(usize, content_length_str.slice(), 10) catch 0; - resp.writeHeaderInt("content-length", len); - this.endWithoutBody(this.shouldCloseConnection()); - return; - } - } - // not content-length or transfer-encoding so we need to respect the body - response.body.value.toBlobIfPossible(); - switch (response.body.value) { - .InternalBlob, .WTFStringImpl => { - var blob = response.body.value.useAsAnyBlobAllowNonUTF8String(); - defer blob.detach(); - const size = blob.size(); - this.renderMetadata(); - - if (size == Blob.max_size) { - resp.writeHeaderInt("content-length", 0); - } else { - resp.writeHeaderInt("content-length", size); - } - this.endWithoutBody(this.shouldCloseConnection()); - }, - - .Blob => |*blob| { - if (blob.isS3()) { - // we need to read the size asynchronously - // in this case should always be a redirect so should not hit this path, but in case we change it in the future lets handle it - this.ref(); - - const credentials = blob.store.?.data.s3.getCredentials(); - const path = blob.store.?.data.s3.path(); - const env = globalThis.bunVM().transpiler.env; - - S3.stat(credentials, path, @ptrCast(&onS3SizeResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); - - return; - } - this.renderMetadata(); - - blob.resolveSize(); - if (blob.size == Blob.max_size) { - resp.writeHeaderInt("content-length", 0); - } else { - resp.writeHeaderInt("content-length", blob.size); - } - this.endWithoutBody(this.shouldCloseConnection()); - }, - .Locked => { - this.renderMetadata(); - resp.writeHeader("transfer-encoding", "chunked"); - this.endWithoutBody(this.shouldCloseConnection()); - }, - .Used, .Null, .Empty, .Error => { - this.renderMetadata(); - resp.writeHeaderInt("content-length", 0); - this.endWithoutBody(this.shouldCloseConnection()); - }, - } - } - - // Each HTTP request or TCP socket connection is effectively a "task". - // - // However, unlike the regular task queue, we don't drain the microtask - // queue at the end. - // - // Instead, we drain it multiple times, at the points that would - // otherwise "halt" the Response from being rendered. - // - // - If you return a Promise, we drain the microtask queue once - // - If you return a streaming Response, we drain the microtask queue (possibly the 2nd time this task!) - pub fn onResponse( - ctx: *RequestContext, - this: *ThisServer, - request_value: JSValue, - response_value: JSValue, - ) void { - request_value.ensureStillAlive(); - response_value.ensureStillAlive(); - ctx.drainMicrotasks(); - - if (ctx.isAbortedOrEnded()) { - return; - } - // if you return a Response object or a Promise - // but you upgraded the connection to a WebSocket - // just ignore the Response object. It doesn't do anything. - // it's better to do that than to throw an error - if (ctx.didUpgradeWebSocket()) { - return; - } - - if (response_value.isEmptyOrUndefinedOrNull()) { - ctx.renderMissingInvalidResponse(response_value); - return; - } - - if (response_value.toError()) |err_value| { - ctx.runErrorHandler(err_value); - return; - } - - if (response_value.as(JSC.WebCore.Response)) |response| { - ctx.response_jsvalue = response_value; - ctx.response_jsvalue.ensureStillAlive(); - ctx.flags.response_protected = false; - if (ctx.method == .HEAD) { - if (ctx.resp) |resp| { - var pair = HeaderResponsePair{ .this = ctx, .response = response }; - resp.runCorkedWithType(*HeaderResponsePair, doRenderHeadResponse, &pair); - } - return; - } else { - response.body.value.toBlobIfPossible(); - - switch (response.body.value) { - .Blob => |*blob| { - if (blob.needsToReadFile()) { - response_value.protect(); - ctx.flags.response_protected = true; - } - }, - .Locked => { - response_value.protect(); - ctx.flags.response_protected = true; - }, - else => {}, - } - ctx.render(response); - } - return; - } - - var vm = this.vm; - - if (response_value.asAnyPromise()) |promise| { - // If we immediately have the value available, we can skip the extra event loop tick - switch (promise.unwrap(vm.global.vm(), .mark_handled)) { - .pending => { - ctx.ref(); - response_value.then(this.globalThis, ctx, RequestContext.onResolve, RequestContext.onReject); - return; - }, - .fulfilled => |fulfilled_value| { - // if you return a Response object or a Promise - // but you upgraded the connection to a WebSocket - // just ignore the Response object. It doesn't do anything. - // it's better to do that than to throw an error - if (ctx.didUpgradeWebSocket()) { - return; - } - - if (fulfilled_value.isEmptyOrUndefinedOrNull()) { - ctx.renderMissingInvalidResponse(fulfilled_value); - return; - } - var response = fulfilled_value.as(JSC.WebCore.Response) orelse { - ctx.renderMissingInvalidResponse(fulfilled_value); - return; - }; - - ctx.response_jsvalue = fulfilled_value; - ctx.response_jsvalue.ensureStillAlive(); - ctx.flags.response_protected = false; - ctx.response_ptr = response; - if (ctx.method == .HEAD) { - if (ctx.resp) |resp| { - var pair = HeaderResponsePair{ .this = ctx, .response = response }; - resp.runCorkedWithType(*HeaderResponsePair, doRenderHeadResponse, &pair); - } - return; - } - response.body.value.toBlobIfPossible(); - switch (response.body.value) { - .Blob => |*blob| { - if (blob.needsToReadFile()) { - fulfilled_value.protect(); - ctx.flags.response_protected = true; - } - }, - .Locked => { - fulfilled_value.protect(); - ctx.flags.response_protected = true; - }, - else => {}, - } - ctx.render(response); - return; - }, - .rejected => |err| { - ctx.handleReject(err); - return; - }, - } - } - } - - pub fn handleResolveStream(req: *RequestContext) void { - streamLog("handleResolveStream", .{}); - - var wrote_anything = false; - if (req.sink) |wrapper| { - req.flags.aborted = req.flags.aborted or wrapper.sink.aborted; - wrote_anything = wrapper.sink.wrote > 0; - - wrapper.sink.finalize(); - wrapper.detach(); - req.sink = null; - wrapper.sink.destroy(); - } - - if (req.response_ptr) |resp| { - assert(req.server != null); - - if (resp.body.value == .Locked) { - const global = resp.body.value.Locked.global; - if (resp.body.value.Locked.readable.get(global)) |stream| { - stream.done(global); - } - resp.body.value.Locked.readable.deinit(); - resp.body.value = .{ .Used = {} }; - } - } - - if (req.isAbortedOrEnded()) { - return; - } - - streamLog("onResolve({any})", .{wrote_anything}); - if (!req.flags.has_written_status) { - req.renderMetadata(); - } - req.endStream(req.shouldCloseConnection()); - } - - pub fn onResolveStream(_: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - streamLog("onResolveStream", .{}); - var args = callframe.arguments_old(2); - var req: *@This() = args.ptr[args.len - 1].asPromisePtr(@This()); - defer req.deref(); - req.handleResolveStream(); - return JSValue.jsUndefined(); - } - pub fn onRejectStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - streamLog("onRejectStream", .{}); - const args = callframe.arguments_old(2); - var req = args.ptr[args.len - 1].asPromisePtr(@This()); - const err = args.ptr[0]; - defer req.deref(); - - req.handleRejectStream(globalThis, err); - return JSValue.jsUndefined(); - } - - pub fn handleRejectStream(req: *@This(), globalThis: *JSC.JSGlobalObject, err: JSValue) void { - streamLog("handleRejectStream", .{}); - - if (req.sink) |wrapper| { - wrapper.sink.pending_flush = null; - wrapper.sink.done = true; - req.flags.aborted = req.flags.aborted or wrapper.sink.aborted; - wrapper.sink.finalize(); - wrapper.detach(); - req.sink = null; - wrapper.sink.destroy(); - } - - if (req.response_ptr) |resp| { - if (resp.body.value == .Locked) { - if (resp.body.value.Locked.readable.get(globalThis)) |stream| { - stream.done(globalThis); - } - resp.body.value.Locked.readable.deinit(); - resp.body.value = .{ .Used = {} }; - } - } - - // aborted so call finalizeForAbort - if (req.isAbortedOrEnded()) { - return; - } - - streamLog("onReject()", .{}); - - if (!req.flags.has_written_status) { - req.renderMetadata(); - } - - if (comptime debug_mode) { - if (req.server) |server| { - if (!err.isEmptyOrUndefinedOrNull()) { - var exception_list: std.ArrayList(Api.JsException) = std.ArrayList(Api.JsException).init(req.allocator); - defer exception_list.deinit(); - server.vm.runErrorHandler(err, &exception_list); - } - } - } - req.endStream(req.shouldCloseConnection()); - } - - pub fn doRenderWithBody(this: *RequestContext, value: *JSC.WebCore.Body.Value) void { - this.drainMicrotasks(); - - // If a ReadableStream can trivially be converted to a Blob, do so. - // If it's a WTFStringImpl and it cannot be used as a UTF-8 string, convert it to a Blob. - value.toBlobIfPossible(); - const globalThis = this.server.?.globalThis; - switch (value.*) { - .Error => |*err_ref| { - _ = value.use(); - if (this.isAbortedOrEnded()) { - return; - } - this.runErrorHandler(err_ref.toJS(globalThis)); - return; - }, - // .InlineBlob, - .WTFStringImpl, - .InternalBlob, - .Blob, - => { - // toBlobIfPossible checks for WTFString needing a conversion. - this.blob = value.useAsAnyBlobAllowNonUTF8String(); - this.renderWithBlobFromBodyValue(); - return; - }, - .Locked => |*lock| { - if (this.isAbortedOrEnded()) { - return; - } - - if (lock.readable.get(globalThis)) |stream_| { - const stream: JSC.WebCore.ReadableStream = stream_; - // we hold the stream alive until we're done with it - this.readable_stream_ref = lock.readable; - value.* = .{ .Used = {} }; - - if (stream.isLocked(globalThis)) { - streamLog("was locked but it shouldn't be", .{}); - var err = JSC.SystemError{ - .code = bun.String.static(@tagName(JSC.Node.ErrorCode.ERR_STREAM_CANNOT_PIPE)), - .message = bun.String.static("Stream already used, please create a new one"), - }; - stream.value.unprotect(); - this.runErrorHandler(err.toErrorInstance(globalThis)); - return; - } - - switch (stream.ptr) { - .Invalid => { - this.readable_stream_ref.deinit(); - }, - // toBlobIfPossible will typically convert .Blob streams, or .File streams into a Blob object, but cannot always. - .Blob, - .File, - // These are the common scenario: - .JavaScript, - .Direct, - => { - if (this.resp) |resp| { - var pair = StreamPair{ .stream = stream, .this = this }; - resp.runCorkedWithType(*StreamPair, doRenderStream, &pair); - } - return; - }, - - .Bytes => |byte_stream| { - assert(byte_stream.pipe.ctx == null); - assert(this.byte_stream == null); - if (this.resp == null) { - // we don't have a response, so we can discard the stream - stream.done(globalThis); - this.readable_stream_ref.deinit(); - return; - } - const resp = this.resp.?; - // If we've received the complete body by the time this function is called - // we can avoid streaming it and just send it all at once. - if (byte_stream.has_received_last_chunk) { - this.blob = .fromArrayList(byte_stream.drain().listManaged(bun.default_allocator)); - this.readable_stream_ref.deinit(); - this.doRenderBlob(); - return; - } - this.ref(); - byte_stream.pipe = JSC.WebCore.Pipe.Wrap(@This(), onPipe).init(this); - this.readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(stream, globalThis); - - this.byte_stream = byte_stream; - this.response_buf_owned = byte_stream.drain().list(); - - // we don't set size here because even if we have a hint - // uWebSockets won't let us partially write streaming content - this.blob.detach(); - - // if we've received metadata and part of the body, send everything we can and drain - if (this.response_buf_owned.items.len > 0) { - resp.runCorkedWithType(*RequestContext, drainResponseBufferAndMetadata, this); - } else { - // if we only have metadata to send, send it now - resp.runCorkedWithType(*RequestContext, renderMetadata, this); - } - return; - }, - } - } - - if (lock.onReceiveValue != null or lock.task != null) { - // someone else is waiting for the stream or waiting for `onStartStreaming` - const readable = value.toReadableStream(globalThis); - readable.ensureStillAlive(); - this.doRenderWithBody(value); - return; - } - - // when there's no stream, we need to - lock.onReceiveValue = doRenderWithBodyLocked; - lock.task = this; - - return; - }, - else => {}, - } - - this.doRenderBlob(); - } - - pub fn onPipe(this: *RequestContext, stream: JSC.WebCore.streams.Result, allocator: std.mem.Allocator) void { - const stream_needs_deinit = stream == .owned or stream == .owned_and_done; - const is_done = stream.isDone(); - defer { - if (is_done) this.deref(); - if (stream_needs_deinit) { - if (is_done) { - stream.owned_and_done.listManaged(allocator).deinit(); - } else { - stream.owned.listManaged(allocator).deinit(); - } - } - } - - if (this.isAbortedOrEnded()) { - return; - } - const resp = this.resp.?; - - const chunk = stream.slice(); - // on failure, it will continue to allocate - // 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) == .want_more) { - if (is_done) { - this.endStream(this.shouldCloseConnection()); - } - } else { - // when it's the last one, we just want to know if it's done - if (is_done) { - this.flags.has_marked_pending = true; - resp.onWritable(*RequestContext, onWritableResponseBuffer, this); - } - } - } - - pub fn doRenderBlob(this: *RequestContext) void { - // We are not corked - // The body is small - // Faster to do the memcpy than to do the two network calls - // We are not streaming - // This is an important performance optimization - if (this.flags.has_abort_handler and this.blob.fastSize() < 16384 - 1024) { - if (this.resp) |resp| { - resp.runCorkedWithType(*RequestContext, doRenderBlobCorked, this); - } - } else { - this.doRenderBlobCorked(); - } - } - - pub fn doRenderBlobCorked(this: *RequestContext) void { - this.renderMetadata(); - this.renderBytes(); - } - - pub fn doRender(this: *RequestContext) void { - ctxLog("doRender", .{}); - - if (this.isAbortedOrEnded()) { - return; - } - var response = this.response_ptr.?; - this.doRenderWithBody(&response.body.value); - } - - pub fn renderProductionError(this: *RequestContext, status: u16) void { - if (this.resp) |resp| { - switch (status) { - 404 => { - if (!this.flags.has_written_status) { - resp.writeStatus("404 Not Found"); - this.flags.has_written_status = true; - } - this.endWithoutBody(this.shouldCloseConnection()); - }, - else => { - if (!this.flags.has_written_status) { - resp.writeStatus("500 Internal Server Error"); - resp.writeHeader("content-type", "text/plain"); - this.flags.has_written_status = true; - } - - this.end("Something went wrong!", this.shouldCloseConnection()); - }, - } - } - } - - pub fn runErrorHandler( - this: *RequestContext, - value: JSC.JSValue, - ) void { - runErrorHandlerWithStatusCode(this, value, 500); - } - - const PathnameFormatter = struct { - ctx: *RequestContext, - - pub fn format(formatter: @This(), comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void { - var this = formatter.ctx; - - if (!this.pathname.isEmpty()) { - try this.pathname.format(fmt, opts, writer); - return; - } - - if (!this.flags.has_abort_handler) { - if (this.req) |req| { - try writer.writeAll(req.url()); - return; - } - } - - try writer.writeAll("/"); - } - }; - - fn ensurePathname(this: *RequestContext) PathnameFormatter { - return .{ .ctx = this }; - } - - pub inline fn shouldCloseConnection(this: *const RequestContext) bool { - if (this.resp) |resp| { - return resp.shouldCloseConnection(); - } - return false; - } - - fn finishRunningErrorHandler(this: *RequestContext, value: JSC.JSValue, status: u16) void { - if (this.server == null) return this.renderProductionError(status); - var vm: *JSC.VirtualMachine = this.server.?.vm; - const globalThis = this.server.?.globalThis; - if (comptime debug_mode) { - var exception_list: std.ArrayList(Api.JsException) = std.ArrayList(Api.JsException).init(this.allocator); - defer exception_list.deinit(); - const prev_exception_list = vm.onUnhandledRejectionExceptionList; - vm.onUnhandledRejectionExceptionList = &exception_list; - vm.onUnhandledRejection(vm, globalThis, value); - vm.onUnhandledRejectionExceptionList = prev_exception_list; - - this.renderDefaultError( - vm.log, - error.ExceptionOcurred, - exception_list.toOwnedSlice() catch @panic("TODO"), - "{s} - {} failed", - .{ @as(string, @tagName(this.method)), this.ensurePathname() }, - ); - } else { - if (status != 404) { - vm.onUnhandledRejection(vm, globalThis, value); - } - this.renderProductionError(status); - } - - vm.log.reset(); - } - - pub fn runErrorHandlerWithStatusCodeDontCheckResponded( - this: *RequestContext, - value: JSC.JSValue, - status: u16, - ) void { - JSC.markBinding(@src()); - if (this.server) |server| { - if (server.config.onError != .zero and !this.flags.has_called_error_handler) { - this.flags.has_called_error_handler = true; - const result = server.config.onError.call( - server.globalThis, - server.js_value.get() orelse .undefined, - &.{value}, - ) catch |err| server.globalThis.takeException(err); - defer result.ensureStillAlive(); - if (!result.isEmptyOrUndefinedOrNull()) { - if (result.toError()) |err| { - this.finishRunningErrorHandler(err, status); - return; - } else if (result.asAnyPromise()) |promise| { - this.processOnErrorPromise(result, promise, value, status); - return; - } else if (result.as(Response)) |response| { - this.render(response); - return; - } - } - } - } - - this.finishRunningErrorHandler(value, status); - } - - fn processOnErrorPromise( - ctx: *RequestContext, - promise_js: JSC.JSValue, - promise: JSC.AnyPromise, - value: JSC.JSValue, - status: u16, - ) void { - assert(ctx.server != null); - var vm = ctx.server.?.vm; - - switch (promise.unwrap(vm.global.vm(), .mark_handled)) { - .pending => { - ctx.flags.is_error_promise_pending = true; - ctx.ref(); - promise_js.then( - ctx.server.?.globalThis, - ctx, - RequestContext.onResolve, - RequestContext.onReject, - ); - }, - .fulfilled => |fulfilled_value| { - // if you return a Response object or a Promise - // but you upgraded the connection to a WebSocket - // just ignore the Response object. It doesn't do anything. - // it's better to do that than to throw an error - if (ctx.didUpgradeWebSocket()) { - return; - } - - var response = fulfilled_value.as(JSC.WebCore.Response) orelse { - ctx.finishRunningErrorHandler(value, status); - return; - }; - - ctx.response_jsvalue = fulfilled_value; - ctx.response_jsvalue.ensureStillAlive(); - ctx.flags.response_protected = false; - ctx.response_ptr = response; - - response.body.value.toBlobIfPossible(); - switch (response.body.value) { - .Blob => |*blob| { - if (blob.needsToReadFile()) { - fulfilled_value.protect(); - ctx.flags.response_protected = true; - } - }, - .Locked => { - fulfilled_value.protect(); - ctx.flags.response_protected = true; - }, - else => {}, - } - ctx.render(response); - return; - }, - .rejected => |err| { - ctx.finishRunningErrorHandler(err, status); - return; - }, - } - } - - pub fn runErrorHandlerWithStatusCode( - this: *RequestContext, - value: JSC.JSValue, - status: u16, - ) void { - JSC.markBinding(@src()); - if (this.resp == null or this.resp.?.hasResponded()) return; - - runErrorHandlerWithStatusCodeDontCheckResponded(this, value, status); - } - - pub fn renderMetadata(this: *RequestContext) void { - if (this.resp == null) return; - const resp = this.resp.?; - - var response: *JSC.WebCore.Response = this.response_ptr.?; - var status = response.statusCode(); - var needs_content_range = this.flags.needs_content_range and this.sendfile.remain < this.blob.size(); - - const size = if (needs_content_range) - this.sendfile.remain - else - this.blob.size(); - - status = if (status == 200 and size == 0 and !this.blob.isDetached()) - 204 - else - status; - - const content_type, const needs_content_type, const content_type_needs_free = getContentType( - response.init.headers, - &this.blob, - this.allocator, - ); - defer if (content_type_needs_free) content_type.deinit(this.allocator); - var has_content_disposition = false; - var has_content_range = false; - if (response.init.headers) |headers_| { - has_content_disposition = headers_.fastHas(.ContentDisposition); - has_content_range = headers_.fastHas(.ContentRange); - needs_content_range = needs_content_range and has_content_range; - if (needs_content_range) { - status = 206; - } - - this.doWriteStatus(status); - this.doWriteHeaders(headers_); - response.init.headers = null; - headers_.deref(); - } else if (needs_content_range) { - status = 206; - this.doWriteStatus(status); - } else { - this.doWriteStatus(status); - } - - if (this.cookies) |cookies| { - this.cookies = null; - defer cookies.deref(); - cookies.write(this.server.?.globalThis, ssl_enabled, @ptrCast(this.resp.?)); - } - - if (needs_content_type and - // do not insert the content type if it is the fallback value - // we may not know the content-type when streaming - (!this.blob.isDetached() or content_type.value.ptr != MimeType.other.value.ptr)) - { - resp.writeHeader("content-type", content_type.value); - } - - // automatically include the filename when: - // 1. Bun.file("foo") - // 2. The content-disposition header is not present - if (!has_content_disposition and content_type.category.autosetFilename()) { - if (this.blob.getFileName()) |filename| { - const basename = std.fs.path.basename(filename); - if (basename.len > 0) { - var filename_buf: [1024]u8 = undefined; - - resp.writeHeader( - "content-disposition", - std.fmt.bufPrint(&filename_buf, "filename=\"{s}\"", .{basename[0..@min(basename.len, 1024 - 32)]}) catch "", - ); - } - } - } - - if (this.flags.needs_content_length) { - resp.writeHeaderInt("content-length", size); - this.flags.needs_content_length = false; - } - - if (needs_content_range and !has_content_range) { - var content_range_buf: [1024]u8 = undefined; - - resp.writeHeader( - "content-range", - std.fmt.bufPrint( - &content_range_buf, - // we omit the full size of the Blob because it could - // change between requests and this potentially leaks - // PII undesirably - "bytes {d}-{d}/*", - .{ this.sendfile.offset, this.sendfile.offset + (this.sendfile.remain -| 1) }, - ) catch "bytes */*", - ); - this.flags.needs_content_range = false; - } - } - - fn doWriteStatus(this: *RequestContext, status: u16) void { - assert(!this.flags.has_written_status); - this.flags.has_written_status = true; - - writeStatus(ssl_enabled, this.resp, status); - } - - fn doWriteHeaders(this: *RequestContext, headers: *WebCore.FetchHeaders) void { - writeHeaders(headers, ssl_enabled, this.resp); - } - - pub fn renderBytes(this: *RequestContext) void { - // copy it to stack memory to prevent aliasing issues in release builds - const blob = this.blob; - const bytes = blob.slice(); - if (this.resp) |resp| { - if (!resp.tryEnd( - bytes, - bytes.len, - this.shouldCloseConnection(), - )) { - this.flags.has_marked_pending = true; - resp.onWritable(*RequestContext, onWritableBytes, this); - return; - } - } - this.detachResponse(); - this.endRequestStreamingAndDrain(); - this.deref(); - } - - pub fn render(this: *RequestContext, response: *JSC.WebCore.Response) void { - ctxLog("render", .{}); - this.response_ptr = response; - - this.doRender(); - } - - pub fn onBufferedBodyChunk(this: *RequestContext, resp: *App.Response, chunk: []const u8, last: bool) void { - ctxLog("onBufferedBodyChunk {} {}", .{ chunk.len, last }); - - assert(this.resp == resp); - - this.flags.is_waiting_for_request_body = last == false; - if (this.isAbortedOrEnded() or this.flags.has_marked_complete) return; - if (!last and chunk.len == 0) { - // Sometimes, we get back an empty chunk - // We have to ignore those chunks unless it's the last one - return; - } - const vm = this.server.?.vm; - const globalThis = this.server.?.globalThis; - - // After the user does request.body, - // if they then do .text(), .arrayBuffer(), etc - // we can no longer hold the strong reference from the body value ref. - if (this.request_body_readable_stream_ref.get(globalThis)) |readable| { - assert(this.request_body_buf.items.len == 0); - vm.eventLoop().enter(); - defer vm.eventLoop().exit(); - - if (!last) { - readable.ptr.Bytes.onData( - .{ - .temporary = bun.ByteList.initConst(chunk), - }, - bun.default_allocator, - ); - } else { - var strong = this.request_body_readable_stream_ref; - this.request_body_readable_stream_ref = .{}; - defer strong.deinit(); - if (this.request_body) |request_body| { - _ = request_body.unref(); - this.request_body = null; - } - - readable.value.ensureStillAlive(); - readable.ptr.Bytes.onData( - .{ - .temporary_and_done = bun.ByteList.initConst(chunk), - }, - bun.default_allocator, - ); - } - - return; - } - - // This is the start of a task, so it's a good time to drain - if (this.request_body != null) { - var body = this.request_body.?; - - if (last) { - var bytes = &this.request_body_buf; - - var old = body.value; - - const total = bytes.items.len + chunk.len; - getter: { - // if (total <= JSC.WebCore.InlineBlob.available_bytes) { - // if (total == 0) { - // body.value = .{ .Empty = {} }; - // break :getter; - // } - - // body.value = .{ .InlineBlob = JSC.WebCore.InlineBlob.concat(bytes.items, chunk) }; - // this.request_body_buf.clearAndFree(this.allocator); - // } else { - bytes.ensureTotalCapacityPrecise(this.allocator, total) catch |err| { - this.request_body_buf.clearAndFree(this.allocator); - body.value.toError(err, globalThis); - break :getter; - }; - - const prev_len = bytes.items.len; - bytes.items.len = total; - var slice = bytes.items[prev_len..]; - @memcpy(slice[0..chunk.len], chunk); - body.value = .{ - .InternalBlob = .{ - .bytes = bytes.toManaged(this.allocator), - }, - }; - // } - } - this.request_body_buf = .{}; - - if (old == .Locked) { - var loop = vm.eventLoop(); - loop.enter(); - defer loop.exit(); - - old.resolve(&body.value, globalThis, null); - } - return; - } - - if (this.request_body_buf.capacity == 0) { - this.request_body_buf.ensureTotalCapacityPrecise(this.allocator, @min(this.request_body_content_len, max_request_body_preallocate_length)) catch @panic("Out of memory while allocating request body buffer"); - } - this.request_body_buf.appendSlice(this.allocator, chunk) catch @panic("Out of memory while allocating request body"); - } - } - - pub fn onStartStreamingRequestBody(this: *RequestContext) JSC.WebCore.DrainResult { - ctxLog("onStartStreamingRequestBody", .{}); - if (this.isAbortedOrEnded()) { - return JSC.WebCore.DrainResult{ - .aborted = {}, - }; - } - - // This means we have received part of the body but not the whole thing - if (this.request_body_buf.items.len > 0) { - var emptied = this.request_body_buf; - this.request_body_buf = .{}; - return .{ - .owned = .{ - .list = emptied.toManaged(this.allocator), - .size_hint = if (emptied.capacity < max_request_body_preallocate_length) - emptied.capacity - else - 0, - }, - }; - } - - return .{ - .estimated_size = this.request_body_content_len, - }; - } - const max_request_body_preallocate_length = 1024 * 256; - pub fn onStartBuffering(this: *RequestContext) void { - if (this.server) |server| { - ctxLog("onStartBuffering", .{}); - // TODO: check if is someone calling onStartBuffering other than onStartBufferingCallback - // if is not, this should be removed and only keep protect + setAbortHandler - if (this.flags.is_transfer_encoding == false and this.request_body_content_len == 0) { - // no content-length or 0 content-length - // no transfer-encoding - if (this.request_body != null) { - var body = this.request_body.?; - var old = body.value; - old.Locked.onReceiveValue = null; - var new_body: WebCore.Body.Value = .{ .Null = {} }; - old.resolve(&new_body, server.globalThis, null); - body.value = new_body; - } - } - } - } - - pub fn onRequestBodyReadableStreamAvailable(ptr: *anyopaque, globalThis: *JSC.JSGlobalObject, readable: JSC.WebCore.ReadableStream) void { - var this = bun.cast(*RequestContext, ptr); - bun.debugAssert(this.request_body_readable_stream_ref.held.impl == null); - this.request_body_readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(readable, globalThis); - } - - pub fn onStartBufferingCallback(this: *anyopaque) void { - onStartBuffering(bun.cast(*RequestContext, this)); - } - - pub fn onStartStreamingRequestBodyCallback(this: *anyopaque) JSC.WebCore.DrainResult { - return onStartStreamingRequestBody(bun.cast(*RequestContext, this)); - } - - pub fn getRemoteSocketInfo(this: *RequestContext) ?uws.SocketAddress { - return (this.resp orelse return null).getRemoteSocketInfo(); - } - - pub fn setTimeout(this: *RequestContext, seconds: c_uint) bool { - if (this.resp) |resp| { - resp.timeout(@min(seconds, 255)); - if (seconds > 0) { - - // we only set the timeout callback if we wanna the timeout event to be triggered - // the connection will be closed so the abort handler will be called after the timeout - if (this.request_weakref.get()) |req| { - if (req.internal_event_callback.hasCallback()) { - this.setTimeoutHandler(); - } - } - } else { - // if the timeout is 0, we don't need to trigger the timeout event - resp.clearTimeout(); - } - return true; - } - return false; - } - - comptime { - const export_prefix = "Bun__HTTPRequestContext" ++ (if (debug_mode) "Debug" else "") ++ (if (ThisServer.ssl_enabled) "TLS" else ""); - if (bun.Environment.export_cpp_apis) { - @export(&JSC.toJSHostFn(onResolve), .{ .name = export_prefix ++ "__onResolve" }); - @export(&JSC.toJSHostFn(onReject), .{ .name = export_prefix ++ "__onReject" }); - @export(&JSC.toJSHostFn(onResolveStream), .{ .name = export_prefix ++ "__onResolveStream" }); - @export(&JSC.toJSHostFn(onRejectStream), .{ .name = export_prefix ++ "__onRejectStream" }); - } - } - }; -} - -pub const WebSocketServer = struct { - globalObject: *JSC.JSGlobalObject = undefined, - handler: WebSocketServer.Handler = .{}, - - maxPayloadLength: u32 = 1024 * 1024 * 16, // 16MB - maxLifetime: u16 = 0, - idleTimeout: u16 = 120, // 2 minutes - compression: i32 = 0, - backpressureLimit: u32 = 1024 * 1024 * 16, // 16MB - sendPingsAutomatically: bool = true, - resetIdleTimeoutOnSend: bool = true, - closeOnBackpressureLimit: bool = false, - - pub const Handler = struct { - onOpen: JSC.JSValue = .zero, - onMessage: JSC.JSValue = .zero, - onClose: JSC.JSValue = .zero, - onDrain: JSC.JSValue = .zero, - onError: JSC.JSValue = .zero, - onPing: JSC.JSValue = .zero, - onPong: JSC.JSValue = .zero, - - app: ?*anyopaque = null, - - // Always set manually. - vm: *JSC.VirtualMachine = undefined, - globalObject: *JSC.JSGlobalObject = undefined, - active_connections: usize = 0, - - /// used by publish() - flags: packed struct(u2) { - ssl: bool = false, - publish_to_self: bool = false, - } = .{}, - - pub fn runErrorCallback(this: *const Handler, vm: *JSC.VirtualMachine, globalObject: *JSC.JSGlobalObject, error_value: JSC.JSValue) void { - const onError = this.onError; - if (!onError.isEmptyOrUndefinedOrNull()) { - _ = onError.call(globalObject, .undefined, &.{error_value}) catch |err| - this.globalObject.reportActiveExceptionAsUnhandled(err); - return; - } - - _ = vm.uncaughtException(globalObject, error_value, false); - } - - pub fn fromJS(globalObject: *JSC.JSGlobalObject, object: JSC.JSValue) bun.JSError!Handler { - var handler = Handler{ .globalObject = globalObject, .vm = VirtualMachine.get() }; - - var valid = false; - - if (try object.getTruthyComptime(globalObject, "message")) |message_| { - if (!message_.isCallable()) { - return globalObject.throwInvalidArguments("websocket expects a function for the message option", .{}); - } - const message = message_.withAsyncContextIfNeeded(globalObject); - handler.onMessage = message; - message.ensureStillAlive(); - valid = true; - } - - if (try object.getTruthy(globalObject, "open")) |open_| { - if (!open_.isCallable()) { - return globalObject.throwInvalidArguments("websocket expects a function for the open option", .{}); - } - const open = open_.withAsyncContextIfNeeded(globalObject); - handler.onOpen = open; - open.ensureStillAlive(); - valid = true; - } - - if (try object.getTruthy(globalObject, "close")) |close_| { - if (!close_.isCallable()) { - return globalObject.throwInvalidArguments("websocket expects a function for the close option", .{}); - } - const close = close_.withAsyncContextIfNeeded(globalObject); - handler.onClose = close; - close.ensureStillAlive(); - valid = true; - } - - if (try object.getTruthy(globalObject, "drain")) |drain_| { - if (!drain_.isCallable()) { - return globalObject.throwInvalidArguments("websocket expects a function for the drain option", .{}); - } - const drain = drain_.withAsyncContextIfNeeded(globalObject); - handler.onDrain = drain; - drain.ensureStillAlive(); - valid = true; - } - - if (try object.getTruthy(globalObject, "onError")) |onError_| { - if (!onError_.isCallable()) { - return globalObject.throwInvalidArguments("websocket expects a function for the onError option", .{}); - } - const onError = onError_.withAsyncContextIfNeeded(globalObject); - handler.onError = onError; - onError.ensureStillAlive(); - } - - if (try object.getTruthy(globalObject, "ping")) |cb| { - if (!cb.isCallable()) { - return globalObject.throwInvalidArguments("websocket expects a function for the ping option", .{}); - } - handler.onPing = cb; - cb.ensureStillAlive(); - valid = true; - } - - if (try object.getTruthy(globalObject, "pong")) |cb| { - if (!cb.isCallable()) { - return globalObject.throwInvalidArguments("websocket expects a function for the pong option", .{}); - } - handler.onPong = cb; - cb.ensureStillAlive(); - valid = true; - } - - if (valid) - return handler; - - return globalObject.throwInvalidArguments("WebSocketServer expects a message handler", .{}); - } - - pub fn protect(this: Handler) void { - this.onOpen.protect(); - this.onMessage.protect(); - this.onClose.protect(); - this.onDrain.protect(); - this.onError.protect(); - this.onPing.protect(); - this.onPong.protect(); - } - - pub fn unprotect(this: Handler) void { - if (this.vm.isShuttingDown()) { - return; - } - - this.onOpen.unprotect(); - this.onMessage.unprotect(); - this.onClose.unprotect(); - this.onDrain.unprotect(); - this.onError.unprotect(); - this.onPing.unprotect(); - this.onPong.unprotect(); - } - }; - - pub fn toBehavior(this: WebSocketServer) uws.WebSocketBehavior { - return .{ - .maxPayloadLength = this.maxPayloadLength, - .idleTimeout = this.idleTimeout, - .compression = this.compression, - .maxBackpressure = this.backpressureLimit, - .sendPingsAutomatically = this.sendPingsAutomatically, - .maxLifetime = this.maxLifetime, - .resetIdleTimeoutOnSend = this.resetIdleTimeoutOnSend, - .closeOnBackpressureLimit = this.closeOnBackpressureLimit, - }; - } - - pub fn protect(this: WebSocketServer) void { - this.handler.protect(); - } - pub fn unprotect(this: WebSocketServer) void { - this.handler.unprotect(); - } - - const CompressTable = bun.ComptimeStringMap(i32, .{ - .{ "disable", 0 }, - .{ "shared", uws.SHARED_COMPRESSOR }, - .{ "dedicated", uws.DEDICATED_COMPRESSOR }, - .{ "3KB", uws.DEDICATED_COMPRESSOR_3KB }, - .{ "4KB", uws.DEDICATED_COMPRESSOR_4KB }, - .{ "8KB", uws.DEDICATED_COMPRESSOR_8KB }, - .{ "16KB", uws.DEDICATED_COMPRESSOR_16KB }, - .{ "32KB", uws.DEDICATED_COMPRESSOR_32KB }, - .{ "64KB", uws.DEDICATED_COMPRESSOR_64KB }, - .{ "128KB", uws.DEDICATED_COMPRESSOR_128KB }, - .{ "256KB", uws.DEDICATED_COMPRESSOR_256KB }, - }); - - const DecompressTable = bun.ComptimeStringMap(i32, .{ - .{ "disable", 0 }, - .{ "shared", uws.SHARED_DECOMPRESSOR }, - .{ "dedicated", uws.DEDICATED_DECOMPRESSOR }, - .{ "3KB", uws.DEDICATED_COMPRESSOR_3KB }, - .{ "4KB", uws.DEDICATED_COMPRESSOR_4KB }, - .{ "8KB", uws.DEDICATED_COMPRESSOR_8KB }, - .{ "16KB", uws.DEDICATED_COMPRESSOR_16KB }, - .{ "32KB", uws.DEDICATED_COMPRESSOR_32KB }, - .{ "64KB", uws.DEDICATED_COMPRESSOR_64KB }, - .{ "128KB", uws.DEDICATED_COMPRESSOR_128KB }, - .{ "256KB", uws.DEDICATED_COMPRESSOR_256KB }, - }); - - pub fn onCreate(globalObject: *JSC.JSGlobalObject, object: JSValue) bun.JSError!WebSocketServer { - var server = WebSocketServer{}; - server.handler = try Handler.fromJS(globalObject, object); - - if (try object.get(globalObject, "perMessageDeflate")) |per_message_deflate| { - getter: { - if (per_message_deflate.isUndefined()) { - break :getter; - } - - if (per_message_deflate.isBoolean() or per_message_deflate.isNull()) { - if (per_message_deflate.toBoolean()) { - server.compression = uws.SHARED_COMPRESSOR | uws.SHARED_DECOMPRESSOR; - } else { - server.compression = 0; - } - break :getter; - } - - if (try per_message_deflate.getTruthy(globalObject, "compress")) |compression| { - if (compression.isBoolean()) { - server.compression |= if (compression.toBoolean()) uws.SHARED_COMPRESSOR else 0; - } else if (compression.isString()) { - server.compression |= CompressTable.getWithEql(try compression.getZigString(globalObject), ZigString.eqlComptime) orelse { - return globalObject.throwInvalidArguments("WebSocketServer expects a valid compress option, either disable \"shared\" \"dedicated\" \"3KB\" \"4KB\" \"8KB\" \"16KB\" \"32KB\" \"64KB\" \"128KB\" or \"256KB\"", .{}); - }; - } else { - return globalObject.throwInvalidArguments("websocket expects a valid compress option, either disable \"shared\" \"dedicated\" \"3KB\" \"4KB\" \"8KB\" \"16KB\" \"32KB\" \"64KB\" \"128KB\" or \"256KB\"", .{}); - } - } - - if (try per_message_deflate.getTruthy(globalObject, "decompress")) |compression| { - if (compression.isBoolean()) { - server.compression |= if (compression.toBoolean()) uws.SHARED_DECOMPRESSOR else 0; - } else if (compression.isString()) { - server.compression |= DecompressTable.getWithEql(try compression.getZigString(globalObject), ZigString.eqlComptime) orelse { - return globalObject.throwInvalidArguments("websocket expects a valid decompress option, either \"disable\" \"shared\" \"dedicated\" \"3KB\" \"4KB\" \"8KB\" \"16KB\" \"32KB\" \"64KB\" \"128KB\" or \"256KB\"", .{}); - }; - } else { - return globalObject.throwInvalidArguments("websocket expects a valid decompress option, either \"disable\" \"shared\" \"dedicated\" \"3KB\" \"4KB\" \"8KB\" \"16KB\" \"32KB\" \"64KB\" \"128KB\" or \"256KB\"", .{}); - } - } - } - } - - if (try object.get(globalObject, "maxPayloadLength")) |value| { - if (!value.isUndefinedOrNull()) { - if (!value.isAnyInt()) { - return globalObject.throwInvalidArguments("websocket expects maxPayloadLength to be an integer", .{}); - } - server.maxPayloadLength = @truncate(@max(value.toInt64(), 0)); - } - } - - if (try object.get(globalObject, "idleTimeout")) |value| { - if (!value.isUndefinedOrNull()) { - if (!value.isAnyInt()) { - return globalObject.throwInvalidArguments("websocket expects idleTimeout to be an integer", .{}); - } - - var idleTimeout: u16 = @truncate(@max(value.toInt64(), 0)); - if (idleTimeout > 960) { - return globalObject.throwInvalidArguments("websocket expects idleTimeout to be 960 or less", .{}); - } else if (idleTimeout > 0) { - // uws does not allow idleTimeout to be between (0, 8), - // since its timer is not that accurate, therefore round up. - idleTimeout = @max(idleTimeout, 8); - } - - server.idleTimeout = idleTimeout; - } - } - if (try object.get(globalObject, "backpressureLimit")) |value| { - if (!value.isUndefinedOrNull()) { - if (!value.isAnyInt()) { - return globalObject.throwInvalidArguments("websocket expects backpressureLimit to be an integer", .{}); - } - - server.backpressureLimit = @truncate(@max(value.toInt64(), 0)); - } - } - - if (try object.get(globalObject, "closeOnBackpressureLimit")) |value| { - if (!value.isUndefinedOrNull()) { - if (!value.isBoolean()) { - return globalObject.throwInvalidArguments("websocket expects closeOnBackpressureLimit to be a boolean", .{}); - } - - server.closeOnBackpressureLimit = value.toBoolean(); - } - } - - if (try object.get(globalObject, "sendPings")) |value| { - if (!value.isUndefinedOrNull()) { - if (!value.isBoolean()) { - return globalObject.throwInvalidArguments("websocket expects sendPings to be a boolean", .{}); - } - - server.sendPingsAutomatically = value.toBoolean(); - } - } - - if (try object.get(globalObject, "publishToSelf")) |value| { - if (!value.isUndefinedOrNull()) { - if (!value.isBoolean()) { - return globalObject.throwInvalidArguments("websocket expects publishToSelf to be a boolean", .{}); - } - - server.handler.flags.publish_to_self = value.toBoolean(); - } - } - - server.protect(); - return server; - } -}; - +pub const ServerConfig = @import("./server/ServerConfig.zig"); pub const ServerWebSocket = @import("./server/ServerWebSocket.zig"); pub const NodeHTTPResponse = @import("./server/NodeHTTPResponse.zig"); @@ -7595,6 +2790,9 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d }; } +pub const AnyRequestContext = @import("./server/AnyRequestContext.zig"); +pub const NewRequestContext = @import("./server/RequestContext.zig").NewRequestContext; + pub const SavedRequest = struct { js_request: JSC.Strong.Optional, request: *Request, @@ -7810,7 +3008,7 @@ pub const AnyServer = struct { }; } - pub fn webSocketHandler(this: AnyServer) ?*WebSocketServer.Handler { + pub fn webSocketHandler(this: AnyServer) ?*WebSocketServerContext.Handler { const server_config: *ServerConfig = switch (this.ptr.tag()) { Ptr.case(HTTPServer) => &this.ptr.as(HTTPServer).config, Ptr.case(HTTPSServer) => &this.ptr.as(HTTPSServer).config, @@ -7932,7 +3130,6 @@ pub const AnyServer = struct { }; } }; -const welcome_page_html_gz = @embedFile("welcome-page.html.gz"); extern fn Bun__addInspector(bool, *anyopaque, *JSC.JSGlobalObject) void; @@ -8078,22 +3275,8 @@ extern fn NodeHTTPServer__onRequest_https( ) JSC.JSValue; extern fn Bun__createNodeHTTPServerSocket(bool, *anyopaque, *JSC.JSGlobalObject) JSC.JSValue; - extern fn NodeHTTP_assignOnCloseFunction(bool, *anyopaque) void; - extern fn NodeHTTP_setUsingCustomExpectHandler(bool, *anyopaque, bool) void; - -fn throwSSLErrorIfNecessary(globalThis: *JSC.JSGlobalObject) bool { - const err_code = BoringSSL.ERR_get_error(); - if (err_code != 0) { - defer BoringSSL.ERR_clear_error(); - globalThis.throwValue(JSC.API.Bun.Crypto.createCryptoError(globalThis, err_code)) catch {}; - return true; - } - - return false; -} - extern "c" fn Bun__ServerRouteList__callRoute( globalObject: *JSC.JSGlobalObject, index: u32, @@ -8110,3 +3293,14 @@ extern "c" fn Bun__ServerRouteList__create( paths: [*]ZigString, pathsLength: usize, ) JSC.JSValue; + +fn throwSSLErrorIfNecessary(globalThis: *JSC.JSGlobalObject) bool { + const err_code = BoringSSL.ERR_get_error(); + if (err_code != 0) { + defer BoringSSL.ERR_clear_error(); + globalThis.throwValue(JSC.API.Bun.Crypto.createCryptoError(globalThis, err_code)) catch {}; + return true; + } + + return false; +} diff --git a/src/bun.js/api/server/AnyRequestContext.zig b/src/bun.js/api/server/AnyRequestContext.zig new file mode 100644 index 0000000000..bf4f50af78 --- /dev/null +++ b/src/bun.js/api/server/AnyRequestContext.zig @@ -0,0 +1,229 @@ +//! A generic wrapper for the HTTP(s) Server`RequestContext`s. +//! Only really exists because of `NewServer()` and `NewRequestContext()` generics. +const AnyRequestContext = @This(); + +pub const Pointer = bun.TaggedPointerUnion(.{ + HTTPServer.RequestContext, + HTTPSServer.RequestContext, + DebugHTTPServer.RequestContext, + DebugHTTPSServer.RequestContext, +}); + +tagged_pointer: Pointer, + +pub const Null: @This() = .{ .tagged_pointer = Pointer.Null }; + +pub fn init(request_ctx: anytype) AnyRequestContext { + return .{ .tagged_pointer = Pointer.init(request_ctx) }; +} + +pub fn memoryCost(self: AnyRequestContext) usize { + if (self.tagged_pointer.isNull()) { + return 0; + } + + switch (self.tagged_pointer.tag()) { + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { + return self.tagged_pointer.as(HTTPServer.RequestContext).memoryCost(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { + return self.tagged_pointer.as(HTTPSServer.RequestContext).memoryCost(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { + return self.tagged_pointer.as(DebugHTTPServer.RequestContext).memoryCost(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { + return self.tagged_pointer.as(DebugHTTPSServer.RequestContext).memoryCost(); + }, + else => @panic("Unexpected AnyRequestContext tag"), + } +} + +pub fn get(self: AnyRequestContext, comptime T: type) ?*T { + return self.tagged_pointer.get(T); +} + +pub fn setTimeout(self: AnyRequestContext, seconds: c_uint) bool { + if (self.tagged_pointer.isNull()) { + return false; + } + + switch (self.tagged_pointer.tag()) { + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { + return self.tagged_pointer.as(HTTPServer.RequestContext).setTimeout(seconds); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { + return self.tagged_pointer.as(HTTPSServer.RequestContext).setTimeout(seconds); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { + return self.tagged_pointer.as(DebugHTTPServer.RequestContext).setTimeout(seconds); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { + return self.tagged_pointer.as(DebugHTTPSServer.RequestContext).setTimeout(seconds); + }, + else => @panic("Unexpected AnyRequestContext tag"), + } + return false; +} + +pub fn setCookies(self: AnyRequestContext, cookie_map: ?*JSC.WebCore.CookieMap) void { + if (self.tagged_pointer.isNull()) { + return; + } + + switch (self.tagged_pointer.tag()) { + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { + return self.tagged_pointer.as(HTTPServer.RequestContext).setCookies(cookie_map); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { + return self.tagged_pointer.as(HTTPSServer.RequestContext).setCookies(cookie_map); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { + return self.tagged_pointer.as(DebugHTTPServer.RequestContext).setCookies(cookie_map); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { + return self.tagged_pointer.as(DebugHTTPSServer.RequestContext).setCookies(cookie_map); + }, + else => @panic("Unexpected AnyRequestContext tag"), + } +} + +pub fn enableTimeoutEvents(self: AnyRequestContext) void { + if (self.tagged_pointer.isNull()) { + return; + } + + switch (self.tagged_pointer.tag()) { + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { + return self.tagged_pointer.as(HTTPServer.RequestContext).setTimeoutHandler(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { + return self.tagged_pointer.as(HTTPSServer.RequestContext).setTimeoutHandler(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { + return self.tagged_pointer.as(DebugHTTPServer.RequestContext).setTimeoutHandler(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { + return self.tagged_pointer.as(DebugHTTPSServer.RequestContext).setTimeoutHandler(); + }, + else => @panic("Unexpected AnyRequestContext tag"), + } +} + +pub fn getRemoteSocketInfo(self: AnyRequestContext) ?uws.SocketAddress { + if (self.tagged_pointer.isNull()) { + return null; + } + + switch (self.tagged_pointer.tag()) { + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { + return self.tagged_pointer.as(HTTPServer.RequestContext).getRemoteSocketInfo(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { + return self.tagged_pointer.as(HTTPSServer.RequestContext).getRemoteSocketInfo(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { + return self.tagged_pointer.as(DebugHTTPServer.RequestContext).getRemoteSocketInfo(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { + return self.tagged_pointer.as(DebugHTTPSServer.RequestContext).getRemoteSocketInfo(); + }, + else => @panic("Unexpected AnyRequestContext tag"), + } +} + +pub fn detachRequest(self: AnyRequestContext) void { + if (self.tagged_pointer.isNull()) { + return; + } + switch (self.tagged_pointer.tag()) { + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { + self.tagged_pointer.as(HTTPServer.RequestContext).req = null; + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { + self.tagged_pointer.as(HTTPSServer.RequestContext).req = null; + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { + self.tagged_pointer.as(DebugHTTPServer.RequestContext).req = null; + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { + self.tagged_pointer.as(DebugHTTPSServer.RequestContext).req = null; + }, + else => @panic("Unexpected AnyRequestContext tag"), + } +} + +/// Wont actually set anything if `self` is `.none` +pub fn setRequest(self: AnyRequestContext, req: *uws.Request) void { + if (self.tagged_pointer.isNull()) { + return; + } + + switch (self.tagged_pointer.tag()) { + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { + self.tagged_pointer.as(HTTPServer.RequestContext).req = req; + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { + self.tagged_pointer.as(HTTPSServer.RequestContext).req = req; + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { + self.tagged_pointer.as(DebugHTTPServer.RequestContext).req = req; + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { + self.tagged_pointer.as(DebugHTTPSServer.RequestContext).req = req; + }, + else => @panic("Unexpected AnyRequestContext tag"), + } +} + +pub fn getRequest(self: AnyRequestContext) ?*uws.Request { + if (self.tagged_pointer.isNull()) { + return null; + } + + switch (self.tagged_pointer.tag()) { + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { + return self.tagged_pointer.as(HTTPServer.RequestContext).req; + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { + return self.tagged_pointer.as(HTTPSServer.RequestContext).req; + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { + return self.tagged_pointer.as(DebugHTTPServer.RequestContext).req; + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { + return self.tagged_pointer.as(DebugHTTPSServer.RequestContext).req; + }, + else => @panic("Unexpected AnyRequestContext tag"), + } +} + +pub fn deref(self: AnyRequestContext) void { + if (self.tagged_pointer.isNull()) { + return; + } + + switch (self.tagged_pointer.tag()) { + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { + self.tagged_pointer.as(HTTPServer.RequestContext).deref(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { + self.tagged_pointer.as(HTTPSServer.RequestContext).deref(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { + self.tagged_pointer.as(DebugHTTPServer.RequestContext).deref(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { + self.tagged_pointer.as(DebugHTTPSServer.RequestContext).deref(); + }, + else => @panic("Unexpected AnyRequestContext tag"), + } +} + +const bun = @import("bun"); +const JSC = bun.JSC; +const uws = bun.uws; +const HTTPServer = @import("../server.zig").HTTPServer; +const HTTPSServer = @import("../server.zig").HTTPSServer; +const DebugHTTPServer = @import("../server.zig").DebugHTTPServer; +const DebugHTTPSServer = @import("../server.zig").DebugHTTPSServer; diff --git a/src/bun.js/api/server/HTTPStatusText.zig b/src/bun.js/api/server/HTTPStatusText.zig new file mode 100644 index 0000000000..83b6bc451c --- /dev/null +++ b/src/bun.js/api/server/HTTPStatusText.zig @@ -0,0 +1,68 @@ +pub fn get(code: u16) ?[]const u8 { + return switch (code) { + 100 => "100 Continue", + 101 => "101 Switching protocols", + 102 => "102 Processing", + 103 => "103 Early Hints", + 200 => "200 OK", + 201 => "201 Created", + 202 => "202 Accepted", + 203 => "203 Non-Authoritative Information", + 204 => "204 No Content", + 205 => "205 Reset Content", + 206 => "206 Partial Content", + 207 => "207 Multi-Status", + 208 => "208 Already Reported", + 226 => "226 IM Used", + 300 => "300 Multiple Choices", + 301 => "301 Moved Permanently", + 302 => "302 Found", + 303 => "303 See Other", + 304 => "304 Not Modified", + 305 => "305 Use Proxy", + 306 => "306 Switch Proxy", + 307 => "307 Temporary Redirect", + 308 => "308 Permanent Redirect", + 400 => "400 Bad Request", + 401 => "401 Unauthorized", + 402 => "402 Payment Required", + 403 => "403 Forbidden", + 404 => "404 Not Found", + 405 => "405 Method Not Allowed", + 406 => "406 Not Acceptable", + 407 => "407 Proxy Authentication Required", + 408 => "408 Request Timeout", + 409 => "409 Conflict", + 410 => "410 Gone", + 411 => "411 Length Required", + 412 => "412 Precondition Failed", + 413 => "413 Payload Too Large", + 414 => "414 URI Too Long", + 415 => "415 Unsupported Media Type", + 416 => "416 Range Not Satisfiable", + 417 => "417 Expectation Failed", + 418 => "418 I'm a Teapot", + 421 => "421 Misdirected Request", + 422 => "422 Unprocessable Entity", + 423 => "423 Locked", + 424 => "424 Failed Dependency", + 425 => "425 Too Early", + 426 => "426 Upgrade Required", + 428 => "428 Precondition Required", + 429 => "429 Too Many Requests", + 431 => "431 Request Header Fields Too Large", + 451 => "451 Unavailable For Legal Reasons", + 500 => "500 Internal Server Error", + 501 => "501 Not Implemented", + 502 => "502 Bad Gateway", + 503 => "503 Service Unavailable", + 504 => "504 Gateway Timeout", + 505 => "505 HTTP Version Not Supported", + 506 => "506 Variant Also Negotiates", + 507 => "507 Insufficient Storage", + 508 => "508 Loop Detected", + 510 => "510 Not Extended", + 511 => "511 Network Authentication Required", + else => null, + }; +} diff --git a/src/bun.js/api/server/RequestContext.zig b/src/bun.js/api/server/RequestContext.zig new file mode 100644 index 0000000000..43f70566bb --- /dev/null +++ b/src/bun.js/api/server/RequestContext.zig @@ -0,0 +1,2539 @@ +pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comptime ThisServer: type) type { + return struct { + const RequestContext = @This(); + + const App = uws.NewApp(ssl_enabled); + pub threadlocal var pool: ?*RequestContext.RequestContextStackAllocator = null; + pub const ResponseStream = JSC.WebCore.HTTPServerWritable(ssl_enabled); + + // This pre-allocates up to 2,048 RequestContext structs. + // It costs about 655,632 bytes. + pub const RequestContextStackAllocator = bun.HiveArray(RequestContext, if (bun.heap_breakdown.enabled) 0 else 2048).Fallback; + + server: ?*ThisServer, + resp: ?*App.Response, + /// thread-local default heap allocator + /// this prevents an extra pthread_getspecific() call which shows up in profiling + allocator: std.mem.Allocator, + req: ?*uws.Request, + request_weakref: Request.WeakRef = .empty, + signal: ?*JSC.WebCore.AbortSignal = null, + method: HTTP.Method, + cookies: ?*JSC.WebCore.CookieMap = null, + + flags: NewFlags(debug_mode) = .{}, + + upgrade_context: ?*uws.SocketContext = null, + + /// We can only safely free once the request body promise is finalized + /// and the response is rejected + response_jsvalue: JSC.JSValue = JSC.JSValue.zero, + ref_count: u8 = 1, + + response_ptr: ?*JSC.WebCore.Response = null, + blob: JSC.WebCore.Blob.Any = JSC.WebCore.Blob.Any{ .Blob = .{} }, + + sendfile: SendfileContext = undefined, + + request_body_readable_stream_ref: JSC.WebCore.ReadableStream.Strong = .{}, + request_body: ?*WebCore.Body.Value.HiveRef = null, + request_body_buf: std.ArrayListUnmanaged(u8) = .{}, + request_body_content_len: usize = 0, + + sink: ?*ResponseStream.JSSink = null, + byte_stream: ?*JSC.WebCore.ByteStream = null, + // reference to the readable stream / byte_stream alive + readable_stream_ref: JSC.WebCore.ReadableStream.Strong = .{}, + + /// Used in errors + pathname: bun.String = bun.String.empty, + + /// Used either for temporary blob data or fallback + /// When the response body is a temporary value + response_buf_owned: std.ArrayListUnmanaged(u8) = .{}, + + /// Defer finalization until after the request handler task is completed? + defer_deinit_until_callback_completes: ?*bool = null, + + // TODO: support builtin compression + const can_sendfile = !ssl_enabled and !Environment.isWindows; + + pub fn memoryCost(this: *const RequestContext) usize { + // The Sink and ByteStream aren't owned by this. + return @sizeOf(RequestContext) + this.request_body_buf.capacity + this.response_buf_owned.capacity + this.blob.memoryCost(); + } + + pub inline fn isAsync(this: *const RequestContext) bool { + return this.defer_deinit_until_callback_completes == null; + } + + fn drainMicrotasks(this: *const RequestContext) void { + if (this.isAsync()) return; + if (this.server) |server| server.vm.drainMicrotasks(); + } + + pub fn setAbortHandler(this: *RequestContext) void { + if (this.flags.has_abort_handler) return; + if (this.resp) |resp| { + this.flags.has_abort_handler = true; + resp.onAborted(*RequestContext, RequestContext.onAbort, this); + } + } + + pub fn setCookies(this: *RequestContext, cookie_map: ?*JSC.WebCore.CookieMap) void { + if (this.cookies) |cookies| cookies.deref(); + this.cookies = cookie_map; + if (this.cookies) |cookies| cookies.ref(); + } + + pub fn setTimeoutHandler(this: *RequestContext) void { + if (this.flags.has_timeout_handler) return; + if (this.resp) |resp| { + this.flags.has_timeout_handler = true; + resp.onTimeout(*RequestContext, RequestContext.onTimeout, this); + } + } + + pub fn onResolve(_: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + ctxLog("onResolve", .{}); + + const arguments = callframe.arguments_old(2); + var ctx = arguments.ptr[1].asPromisePtr(@This()); + defer ctx.deref(); + + const result = arguments.ptr[0]; + result.ensureStillAlive(); + + handleResolve(ctx, result); + return JSValue.jsUndefined(); + } + + fn renderMissingInvalidResponse(ctx: *RequestContext, value: JSC.JSValue) void { + const class_name = value.getClassInfoName() orelse ""; + + if (ctx.server) |server| { + const globalThis: *JSC.JSGlobalObject = server.globalThis; + + Output.enableBuffering(); + var writer = Output.errorWriter(); + + if (bun.strings.eqlComptime(class_name, "Response")) { + Output.errGeneric("Expected a native Response object, but received a polyfilled Response object. Bun.serve() only supports native Response objects.", .{}); + } else if (value != .zero and !globalThis.hasException()) { + var formatter = JSC.ConsoleObject.Formatter{ + .globalThis = globalThis, + .quote_strings = true, + }; + defer formatter.deinit(); + Output.errGeneric("Expected a Response object, but received '{}'", .{value.toFmt(&formatter)}); + } else { + Output.errGeneric("Expected a Response object", .{}); + } + + Output.flush(); + if (!globalThis.hasException()) { + JSC.ConsoleObject.writeTrace(@TypeOf(&writer), &writer, globalThis); + } + Output.flush(); + } + ctx.renderMissing(); + } + + fn handleResolve(ctx: *RequestContext, value: JSC.JSValue) void { + if (ctx.isAbortedOrEnded() or ctx.didUpgradeWebSocket()) { + return; + } + + if (ctx.server == null) { + ctx.renderMissingInvalidResponse(value); + return; + } + if (value.isEmptyOrUndefinedOrNull() or !value.isCell()) { + ctx.renderMissingInvalidResponse(value); + return; + } + + const response = value.as(JSC.WebCore.Response) orelse { + ctx.renderMissingInvalidResponse(value); + return; + }; + ctx.response_jsvalue = value; + assert(!ctx.flags.response_protected); + ctx.flags.response_protected = true; + value.protect(); + + if (ctx.method == .HEAD) { + if (ctx.resp) |resp| { + var pair = HeaderResponsePair{ .this = ctx, .response = response }; + resp.runCorkedWithType(*HeaderResponsePair, doRenderHeadResponse, &pair); + } + return; + } + + ctx.render(response); + } + + pub fn shouldRenderMissing(this: *RequestContext) bool { + // If we did not respond yet, we should render missing + // To allow this all the conditions above should be true: + // 1 - still has a response (not detached) + // 2 - not aborted + // 3 - not marked completed + // 4 - not marked pending + // 5 - is the only reference of the context + // 6 - is not waiting for request body + // 7 - did not call sendfile + return this.resp != null and !this.flags.aborted and !this.flags.has_marked_complete and !this.flags.has_marked_pending and this.ref_count == 1 and !this.flags.is_waiting_for_request_body and !this.flags.has_sendfile_ctx; + } + + pub fn isDeadRequest(this: *RequestContext) bool { + // check if has pending promise or extra reference (aka not the only reference) + if (this.ref_count > 1) return false; + // check if the body is Locked (streaming) + if (this.request_body) |body| { + if (body.value == .Locked) { + return false; + } + } + + return true; + } + + /// destroy RequestContext, should be only called by deref or if defer_deinit_until_callback_completes is ref is set to true + pub fn deinit(this: *RequestContext) void { + this.detachResponse(); + this.endRequestStreamingAndDrain(); + // TODO: has_marked_complete is doing something? + this.flags.has_marked_complete = true; + + if (this.defer_deinit_until_callback_completes) |defer_deinit| { + defer_deinit.* = true; + ctxLog("deferred deinit ({*})", .{this}); + return; + } + + ctxLog("deinit ({*})", .{this}); + if (comptime Environment.isDebug) + assert(this.flags.has_finalized); + + this.request_body_buf.clearAndFree(this.allocator); + this.response_buf_owned.clearAndFree(this.allocator); + + if (this.request_body) |body| { + _ = body.unref(); + this.request_body = null; + } + + if (this.server) |server| { + this.server = null; + server.request_pool_allocator.put(this); + server.onRequestComplete(); + } + } + + pub fn deref(this: *RequestContext) void { + streamLog("deref", .{}); + assert(this.ref_count > 0); + const ref_count = this.ref_count; + this.ref_count -= 1; + if (ref_count == 1) { + this.finalizeWithoutDeinit(); + this.deinit(); + } + } + + pub fn ref(this: *RequestContext) void { + streamLog("ref", .{}); + this.ref_count += 1; + } + + pub fn onReject(_: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + ctxLog("onReject", .{}); + + const arguments = callframe.arguments_old(2); + const ctx = arguments.ptr[1].asPromisePtr(@This()); + const err = arguments.ptr[0]; + defer ctx.deref(); + handleReject(ctx, if (!err.isEmptyOrUndefinedOrNull()) err else .undefined); + return JSValue.jsUndefined(); + } + + fn handleReject(ctx: *RequestContext, value: JSC.JSValue) void { + if (ctx.isAbortedOrEnded()) { + return; + } + + const resp = ctx.resp.?; + const has_responded = resp.hasResponded(); + if (!has_responded) { + const original_state = ctx.defer_deinit_until_callback_completes; + var should_deinit_context = if (original_state) |defer_deinit| defer_deinit.* else false; + ctx.defer_deinit_until_callback_completes = &should_deinit_context; + ctx.runErrorHandler( + value, + ); + ctx.defer_deinit_until_callback_completes = original_state; + // we try to deinit inside runErrorHandler so we just return here and let it deinit + if (should_deinit_context) { + ctx.deinit(); + return; + } + } + // check again in case it get aborted after runErrorHandler + if (ctx.isAbortedOrEnded()) { + return; + } + + // I don't think this case happens? + if (ctx.didUpgradeWebSocket()) { + return; + } + + if (!resp.hasResponded() and !ctx.flags.has_marked_pending and !ctx.flags.is_error_promise_pending) { + ctx.renderMissing(); + return; + } + } + + pub fn renderMissing(ctx: *RequestContext) void { + if (ctx.resp) |resp| { + resp.runCorkedWithType(*RequestContext, renderMissingCorked, ctx); + } + } + + pub fn renderMissingCorked(ctx: *RequestContext) void { + if (ctx.resp) |resp| { + if (comptime !debug_mode) { + if (!ctx.flags.has_written_status) + resp.writeStatus("204 No Content"); + ctx.flags.has_written_status = true; + ctx.end("", ctx.shouldCloseConnection()); + return; + } + // avoid writing the status again and mismatching the content-length + if (ctx.flags.has_written_status) { + ctx.end("", ctx.shouldCloseConnection()); + return; + } + + if (ctx.flags.is_web_browser_navigation) { + resp.writeStatus("200 OK"); + ctx.flags.has_written_status = true; + + resp.writeHeader("content-type", MimeType.html.value); + resp.writeHeader("content-encoding", "gzip"); + resp.writeHeaderInt("content-length", welcome_page_html_gz.len); + ctx.end(welcome_page_html_gz, ctx.shouldCloseConnection()); + return; + } + const missing_content = "Welcome to Bun! To get started, return a Response object."; + resp.writeStatus("200 OK"); + resp.writeHeader("content-type", MimeType.text.value); + resp.writeHeaderInt("content-length", missing_content.len); + ctx.flags.has_written_status = true; + ctx.end(missing_content, ctx.shouldCloseConnection()); + } + } + + pub fn renderDefaultError( + this: *RequestContext, + log: *logger.Log, + err: anyerror, + exceptions: []Api.JsException, + comptime fmt: string, + args: anytype, + ) void { + if (!this.flags.has_written_status) { + this.flags.has_written_status = true; + if (this.resp) |resp| { + resp.writeStatus("500 Internal Server Error"); + resp.writeHeader("content-type", MimeType.html.value); + } + } + + const allocator = this.allocator; + + const fallback_container = allocator.create(Api.FallbackMessageContainer) catch unreachable; + defer allocator.destroy(fallback_container); + fallback_container.* = Api.FallbackMessageContainer{ + .message = std.fmt.allocPrint(allocator, comptime Output.prettyFmt(fmt, false), args) catch unreachable, + .router = null, + .reason = .fetch_event_handler, + .cwd = VirtualMachine.get().transpiler.fs.top_level_dir, + .problems = Api.Problems{ + .code = @as(u16, @truncate(@intFromError(err))), + .name = @errorName(err), + .exceptions = exceptions, + .build = log.toAPI(allocator) catch unreachable, + }, + }; + + if (comptime fmt.len > 0) Output.prettyErrorln(fmt, args); + Output.flush(); + + var bb = std.ArrayList(u8).init(allocator); + const bb_writer = bb.writer(); + + Fallback.renderBackend( + allocator, + fallback_container, + @TypeOf(bb_writer), + bb_writer, + ) catch unreachable; + if (this.resp == null or this.resp.?.tryEnd(bb.items, bb.items.len, this.shouldCloseConnection())) { + bb.clearAndFree(); + this.detachResponse(); + this.endRequestStreamingAndDrain(); + this.finalizeWithoutDeinit(); + this.deref(); + return; + } + + this.flags.has_marked_pending = true; + this.response_buf_owned = std.ArrayListUnmanaged(u8){ .items = bb.items, .capacity = bb.capacity }; + + if (this.resp) |resp| { + resp.onWritable(*RequestContext, onWritableCompleteResponseBuffer, this); + } + } + + pub fn renderResponseBuffer(this: *RequestContext) void { + if (this.resp) |resp| { + resp.onWritable(*RequestContext, onWritableResponseBuffer, this); + } + } + + /// Render a complete response buffer + pub fn renderResponseBufferAndMetadata(this: *RequestContext) void { + if (this.resp) |resp| { + this.renderMetadata(); + + if (!resp.tryEnd( + this.response_buf_owned.items, + this.response_buf_owned.items.len, + this.shouldCloseConnection(), + )) { + this.flags.has_marked_pending = true; + resp.onWritable(*RequestContext, onWritableCompleteResponseBuffer, this); + return; + } + } + this.detachResponse(); + this.endRequestStreamingAndDrain(); + this.deref(); + } + + /// Drain a partial response buffer + pub fn drainResponseBufferAndMetadata(this: *RequestContext) void { + if (this.resp) |resp| { + this.renderMetadata(); + + _ = resp.write( + this.response_buf_owned.items, + ); + } + this.response_buf_owned.items.len = 0; + } + + pub fn end(this: *RequestContext, data: []const u8, closeConnection: bool) void { + if (this.resp) |resp| { + defer this.deref(); + + this.detachResponse(); + this.endRequestStreamingAndDrain(); + resp.end(data, closeConnection); + } + } + + pub fn endStream(this: *RequestContext, closeConnection: bool) void { + ctxLog("endStream", .{}); + if (this.resp) |resp| { + defer this.deref(); + + this.detachResponse(); + this.endRequestStreamingAndDrain(); + // This will send a terminating 0\r\n\r\n chunk to the client + // We only want to do that if they're still expecting a body + // We cannot call this function if the Content-Length header was previously set + if (resp.state().isResponsePending()) + resp.endStream(closeConnection); + } + } + + pub fn endWithoutBody(this: *RequestContext, closeConnection: bool) void { + if (this.resp) |resp| { + defer this.deref(); + + this.detachResponse(); + this.endRequestStreamingAndDrain(); + resp.endWithoutBody(closeConnection); + } + } + + pub fn onWritableResponseBuffer(this: *RequestContext, _: u64, resp: *App.Response) bool { + ctxLog("onWritableResponseBuffer", .{}); + + assert(this.resp == resp); + if (this.isAbortedOrEnded()) { + return false; + } + this.end("", this.shouldCloseConnection()); + return false; + } + + // TODO: should we cork? + pub fn onWritableCompleteResponseBufferAndMetadata(this: *RequestContext, write_offset: u64, resp: *App.Response) bool { + ctxLog("onWritableCompleteResponseBufferAndMetadata", .{}); + assert(this.resp == resp); + + if (this.isAbortedOrEnded()) { + return false; + } + + if (!this.flags.has_written_status) { + this.renderMetadata(); + } + + if (this.method == .HEAD) { + this.endWithoutBody(this.shouldCloseConnection()); + return false; + } + + return this.sendWritableBytesForCompleteResponseBuffer(this.response_buf_owned.items, write_offset, resp); + } + + pub fn onWritableCompleteResponseBuffer(this: *RequestContext, write_offset: u64, resp: *App.Response) bool { + ctxLog("onWritableCompleteResponseBuffer", .{}); + assert(this.resp == resp); + if (this.isAbortedOrEnded()) { + return false; + } + return this.sendWritableBytesForCompleteResponseBuffer(this.response_buf_owned.items, write_offset, resp); + } + + pub fn create(this: *RequestContext, server: *ThisServer, req: *uws.Request, resp: *App.Response, should_deinit_context: ?*bool, method: ?bun.http.Method) void { + this.* = .{ + .allocator = server.allocator, + .resp = resp, + .req = req, + .method = method orelse HTTP.Method.which(req.method()) orelse .GET, + .server = server, + .defer_deinit_until_callback_completes = should_deinit_context, + }; + + ctxLog("create ({*})", .{this}); + } + + pub fn onTimeout(this: *RequestContext, resp: *App.Response) void { + assert(this.resp == resp); + assert(this.server != null); + + var any_js_calls = false; + var vm = this.server.?.vm; + const globalThis = this.server.?.globalThis; + defer { + // This is a task in the event loop. + // If we called into JavaScript, we must drain the microtask queue + if (any_js_calls) { + vm.drainMicrotasks(); + } + } + + if (this.request_weakref.get()) |request| { + if (request.internal_event_callback.trigger(Request.InternalJSEventCallback.EventType.timeout, globalThis)) { + any_js_calls = true; + } + } + } + + pub fn onAbort(this: *RequestContext, resp: *App.Response) void { + assert(this.resp == resp); + assert(!this.flags.aborted); + assert(this.server != null); + // mark request as aborted + this.flags.aborted = true; + + this.detachResponse(); + var any_js_calls = false; + var vm = this.server.?.vm; + const globalThis = this.server.?.globalThis; + defer { + // This is a task in the event loop. + // If we called into JavaScript, we must drain the microtask queue + if (any_js_calls) { + vm.drainMicrotasks(); + } + this.deref(); + } + + if (this.request_weakref.get()) |request| { + request.request_context = AnyRequestContext.Null; + if (request.internal_event_callback.trigger(Request.InternalJSEventCallback.EventType.abort, globalThis)) { + any_js_calls = true; + } + // we can already clean this strong refs + request.internal_event_callback.deinit(); + this.request_weakref.deref(); + } + // if signal is not aborted, abort the signal + if (this.signal) |signal| { + this.signal = null; + defer { + signal.pendingActivityUnref(); + signal.unref(); + } + if (!signal.aborted()) { + signal.signal(globalThis, .ConnectionClosed); + any_js_calls = true; + } + } + + //if have sink, call onAborted on sink + if (this.sink) |wrapper| { + wrapper.sink.abort(); + return; + } + + // if we can, free the request now. + if (this.isDeadRequest()) { + this.finalizeWithoutDeinit(); + } else { + if (this.endRequestStreaming()) { + any_js_calls = true; + } + + if (this.response_ptr) |response| { + if (response.body.value == .Locked) { + var strong_readable = response.body.value.Locked.readable; + response.body.value.Locked.readable = .{}; + defer strong_readable.deinit(); + if (strong_readable.get(globalThis)) |readable| { + readable.abort(globalThis); + any_js_calls = true; + } + } + } + } + } + + // This function may be called multiple times + // so it's important that we can safely do that + pub fn finalizeWithoutDeinit(this: *RequestContext) void { + ctxLog("finalizeWithoutDeinit ({*})", .{this}); + this.blob.detach(); + assert(this.server != null); + const globalThis = this.server.?.globalThis; + + if (comptime Environment.isDebug) { + ctxLog("finalizeWithoutDeinit: has_finalized {any}", .{this.flags.has_finalized}); + this.flags.has_finalized = true; + } + + if (this.response_jsvalue != .zero) { + ctxLog("finalizeWithoutDeinit: response_jsvalue != .zero", .{}); + if (this.flags.response_protected) { + this.response_jsvalue.unprotect(); + this.flags.response_protected = false; + } + this.response_jsvalue = JSC.JSValue.zero; + } + + this.request_body_readable_stream_ref.deinit(); + + if (this.cookies) |cookies| { + this.cookies = null; + cookies.deref(); + } + + if (this.request_weakref.get()) |request| { + request.request_context = AnyRequestContext.Null; + // we can already clean this strong refs + request.internal_event_callback.deinit(); + this.request_weakref.deref(); + } + + // if signal is not aborted, abort the signal + if (this.signal) |signal| { + this.signal = null; + defer { + signal.pendingActivityUnref(); + signal.unref(); + } + if (this.flags.aborted and !signal.aborted()) { + signal.signal(globalThis, .ConnectionClosed); + } + } + + // Case 1: + // User called .blob(), .json(), text(), or .arrayBuffer() on the Request object + // but we received nothing or the connection was aborted + // the promise is pending + // Case 2: + // User ignored the body and the connection was aborted or ended + // Case 3: + // Stream was not consumed and the connection was aborted or ended + _ = this.endRequestStreaming(); + + if (this.byte_stream) |stream| { + ctxLog("finalizeWithoutDeinit: stream != null", .{}); + + this.byte_stream = null; + stream.unpipeWithoutDeref(); + } + + this.readable_stream_ref.deinit(); + + if (!this.pathname.isEmpty()) { + this.pathname.deref(); + this.pathname = bun.String.empty; + } + } + + pub fn endSendFile(this: *RequestContext, writeOffSet: usize, closeConnection: bool) void { + if (this.resp) |resp| { + defer this.deref(); + + this.detachResponse(); + this.endRequestStreamingAndDrain(); + resp.endSendFile(writeOffSet, closeConnection); + } + } + + fn cleanupAndFinalizeAfterSendfile(this: *RequestContext) void { + const sendfile = this.sendfile; + this.endSendFile(sendfile.offset, this.shouldCloseConnection()); + + // use node syscall so that we don't segfault on BADF + if (sendfile.auto_close) + sendfile.fd.close(); + } + const separator: string = "\r\n"; + const separator_iovec = [1]std.posix.iovec_const{.{ + .iov_base = separator.ptr, + .iov_len = separator.len, + }}; + + pub fn onSendfile(this: *RequestContext) bool { + if (this.isAbortedOrEnded()) { + this.cleanupAndFinalizeAfterSendfile(); + return false; + } + const resp = this.resp.?; + + const adjusted_count_temporary = @min(@as(u64, this.sendfile.remain), @as(u63, std.math.maxInt(u63))); + // TODO we should not need this int cast; improve the return type of `@min` + const adjusted_count = @as(u63, @intCast(adjusted_count_temporary)); + + if (Environment.isLinux) { + var signed_offset = @as(i64, @intCast(this.sendfile.offset)); + const start = this.sendfile.offset; + const val = linux.sendfile(this.sendfile.socket_fd.cast(), this.sendfile.fd.cast(), &signed_offset, this.sendfile.remain); + this.sendfile.offset = @as(Blob.SizeType, @intCast(signed_offset)); + + const errcode = bun.sys.getErrno(val); + + this.sendfile.remain -|= @as(Blob.SizeType, @intCast(this.sendfile.offset -| start)); + + if (errcode != .SUCCESS or this.isAbortedOrEnded() or this.sendfile.remain == 0 or val == 0) { + if (errcode != .AGAIN and errcode != .SUCCESS and errcode != .PIPE and errcode != .NOTCONN) { + Output.prettyErrorln("Error: {s}", .{@tagName(errcode)}); + Output.flush(); + } + this.cleanupAndFinalizeAfterSendfile(); + return errcode != .SUCCESS; + } + } else { + var sbytes: std.posix.off_t = adjusted_count; + const signed_offset = @as(i64, @bitCast(@as(u64, this.sendfile.offset))); + const errcode = bun.sys.getErrno(std.c.sendfile( + this.sendfile.fd.cast(), + this.sendfile.socket_fd.cast(), + signed_offset, + &sbytes, + null, + 0, + )); + const wrote = @as(Blob.SizeType, @intCast(sbytes)); + this.sendfile.offset +|= wrote; + this.sendfile.remain -|= wrote; + if (errcode != .AGAIN or this.isAbortedOrEnded() or this.sendfile.remain == 0 or sbytes == 0) { + if (errcode != .AGAIN and errcode != .SUCCESS and errcode != .PIPE and errcode != .NOTCONN) { + Output.prettyErrorln("Error: {s}", .{@tagName(errcode)}); + Output.flush(); + } + this.cleanupAndFinalizeAfterSendfile(); + return errcode == .SUCCESS; + } + } + + if (!this.sendfile.has_set_on_writable) { + this.sendfile.has_set_on_writable = true; + this.flags.has_marked_pending = true; + resp.onWritable(*RequestContext, onWritableSendfile, this); + } + + resp.markNeedsMore(); + + return true; + } + + pub fn onWritableBytes(this: *RequestContext, write_offset: u64, resp: *App.Response) bool { + ctxLog("onWritableBytes", .{}); + assert(this.resp == resp); + if (this.isAbortedOrEnded()) { + return false; + } + + // Copy to stack memory to prevent aliasing issues in release builds + const blob = this.blob; + const bytes = blob.slice(); + + _ = this.sendWritableBytesForBlob(bytes, write_offset, resp); + return true; + } + + pub fn sendWritableBytesForBlob(this: *RequestContext, bytes_: []const u8, write_offset_: u64, resp: *App.Response) bool { + assert(this.resp == resp); + const write_offset: usize = write_offset_; + + const bytes = bytes_[@min(bytes_.len, @as(usize, @truncate(write_offset)))..]; + if (resp.tryEnd(bytes, bytes_.len, this.shouldCloseConnection())) { + this.detachResponse(); + this.endRequestStreamingAndDrain(); + this.deref(); + return true; + } else { + this.flags.has_marked_pending = true; + resp.onWritable(*RequestContext, onWritableBytes, this); + return true; + } + } + + pub fn sendWritableBytesForCompleteResponseBuffer(this: *RequestContext, bytes_: []const u8, write_offset_: u64, resp: *App.Response) bool { + const write_offset: usize = write_offset_; + assert(this.resp == resp); + + const bytes = bytes_[@min(bytes_.len, @as(usize, @truncate(write_offset)))..]; + if (resp.tryEnd(bytes, bytes_.len, this.shouldCloseConnection())) { + this.response_buf_owned.items.len = 0; + this.detachResponse(); + this.endRequestStreamingAndDrain(); + this.deref(); + } else { + this.flags.has_marked_pending = true; + resp.onWritable(*RequestContext, onWritableCompleteResponseBuffer, this); + } + + return true; + } + + pub fn onWritableSendfile(this: *RequestContext, _: u64, _: *App.Response) bool { + ctxLog("onWritableSendfile", .{}); + return this.onSendfile(); + } + + // We tried open() in another thread for this + // it was not faster due to the mountain of syscalls + pub fn renderSendFile(this: *RequestContext, blob: JSC.WebCore.Blob) void { + if (this.resp == null or this.server == null) return; + const globalThis = this.server.?.globalThis; + const resp = this.resp.?; + + this.blob = .{ .Blob = blob }; + const file = &this.blob.store().?.data.file; + var file_buf: bun.PathBuffer = undefined; + const auto_close = file.pathlike != .fd; + const fd = if (!auto_close) + file.pathlike.fd + else switch (bun.sys.open(file.pathlike.path.sliceZ(&file_buf), bun.O.RDONLY | bun.O.NONBLOCK | bun.O.CLOEXEC, 0)) { + .result => |_fd| _fd, + .err => |err| return this.runErrorHandler(err.withPath(file.pathlike.path.slice()).toJSC(globalThis)), + }; + + // stat only blocks if the target is a file descriptor + const stat: bun.Stat = switch (bun.sys.fstat(fd)) { + .result => |result| result, + .err => |err| { + this.runErrorHandler(err.withPathLike(file.pathlike).toJSC(globalThis)); + if (auto_close) { + fd.close(); + } + return; + }, + }; + + if (Environment.isMac) { + if (!bun.isRegularFile(stat.mode)) { + if (auto_close) { + fd.close(); + } + + var err = bun.sys.Error{ + .errno = @as(bun.sys.Error.Int, @intCast(@intFromEnum(std.posix.E.INVAL))), + .syscall = .sendfile, + }; + var sys = err.withPathLike(file.pathlike).toSystemError(); + sys.message = bun.String.static("MacOS does not support sending non-regular files"); + this.runErrorHandler(sys.toErrorInstance( + globalThis, + )); + return; + } + } + + if (Environment.isLinux) { + if (!(bun.isRegularFile(stat.mode) or std.posix.S.ISFIFO(stat.mode) or std.posix.S.ISSOCK(stat.mode))) { + if (auto_close) { + fd.close(); + } + + var err = bun.sys.Error{ + .errno = @as(bun.sys.Error.Int, @intCast(@intFromEnum(std.posix.E.INVAL))), + .syscall = .sendfile, + }; + var sys = err.withPathLike(file.pathlike).toShellSystemError(); + sys.message = bun.String.static("File must be regular or FIFO"); + this.runErrorHandler(sys.toErrorInstance(globalThis)); + return; + } + } + + const original_size = this.blob.Blob.size; + const stat_size = @as(Blob.SizeType, @intCast(stat.size)); + this.blob.Blob.size = if (bun.isRegularFile(stat.mode)) + stat_size + else + @min(original_size, stat_size); + + this.flags.needs_content_length = true; + + this.sendfile = .{ + .fd = fd, + .remain = this.blob.Blob.offset + original_size, + .offset = this.blob.Blob.offset, + .auto_close = auto_close, + .socket_fd = if (!this.isAbortedOrEnded()) resp.getNativeHandle() else bun.invalid_fd, + }; + + // if we are sending only part of a file, include the content-range header + // only include content-range automatically when using a file path instead of an fd + // this is to better support manually controlling the behavior + if (bun.isRegularFile(stat.mode) and auto_close) { + this.flags.needs_content_range = (this.sendfile.remain -| this.sendfile.offset) != stat_size; + } + + // we know the bounds when we are sending a regular file + if (bun.isRegularFile(stat.mode)) { + this.sendfile.offset = @min(this.sendfile.offset, stat_size); + this.sendfile.remain = @min(@max(this.sendfile.remain, this.sendfile.offset), stat_size) -| this.sendfile.offset; + } + + resp.runCorkedWithType(*RequestContext, renderMetadataAndNewline, this); + + if (this.sendfile.remain == 0 or !this.method.hasBody()) { + this.cleanupAndFinalizeAfterSendfile(); + return; + } + + _ = this.onSendfile(); + } + + pub fn renderMetadataAndNewline(this: *RequestContext) void { + if (this.resp) |resp| { + this.renderMetadata(); + resp.prepareForSendfile(); + } + } + + pub fn doSendfile(this: *RequestContext, blob: Blob) void { + if (this.isAbortedOrEnded()) { + return; + } + + if (this.flags.has_sendfile_ctx) return; + + this.flags.has_sendfile_ctx = true; + + if (comptime can_sendfile) { + return this.renderSendFile(blob); + } + if (this.server) |server| { + this.ref(); + this.blob.Blob.doReadFileInternal(*RequestContext, this, onReadFile, server.globalThis); + } + } + + pub fn onReadFile(this: *RequestContext, result: Blob.read_file.ReadFileResultType) void { + defer this.deref(); + + if (this.isAbortedOrEnded()) { + return; + } + + if (result == .err) { + if (this.server) |server| { + this.runErrorHandler(result.err.toErrorInstance(server.globalThis)); + } + return; + } + + const is_temporary = result.result.is_temporary; + + if (comptime Environment.allow_assert) { + assert(this.blob == .Blob); + } + + if (!is_temporary) { + this.blob.Blob.resolveSize(); + this.doRenderBlob(); + } else { + const stat_size = @as(Blob.SizeType, @intCast(result.result.total_size)); + + if (this.blob == .Blob) { + const original_size = this.blob.Blob.size; + // if we dont know the size we use the stat size + this.blob.Blob.size = if (original_size == 0 or original_size == Blob.max_size) + stat_size + else // the blob can be a slice of a file + @max(original_size, stat_size); + } + + if (!this.flags.has_written_status) + this.flags.needs_content_range = true; + + // this is used by content-range + this.sendfile = .{ + .fd = bun.invalid_fd, + .remain = @as(Blob.SizeType, @truncate(result.result.buf.len)), + .offset = if (this.blob == .Blob) this.blob.Blob.offset else 0, + .auto_close = false, + .socket_fd = bun.invalid_fd, + }; + + this.response_buf_owned = .{ .items = result.result.buf, .capacity = result.result.buf.len }; + this.resp.?.runCorkedWithType(*RequestContext, renderResponseBufferAndMetadata, this); + } + } + + pub fn doRenderWithBodyLocked(this: *anyopaque, value: *JSC.WebCore.Body.Value) void { + doRenderWithBody(bun.cast(*RequestContext, this), value); + } + + fn renderWithBlobFromBodyValue(this: *RequestContext) void { + if (this.isAbortedOrEnded()) { + return; + } + + if (this.blob.needsToReadFile()) { + if (!this.flags.has_sendfile_ctx) + this.doSendfile(this.blob.Blob); + return; + } + + this.doRenderBlob(); + } + + const StreamPair = struct { this: *RequestContext, stream: JSC.WebCore.ReadableStream }; + + fn handleFirstStreamWrite(this: *@This()) void { + if (!this.flags.has_written_status) { + this.renderMetadata(); + } + } + + fn doRenderStream(pair: *StreamPair) void { + ctxLog("doRenderStream", .{}); + var this = pair.this; + var stream = pair.stream; + assert(this.server != null); + const globalThis = this.server.?.globalThis; + + if (this.isAbortedOrEnded()) { + stream.cancel(globalThis); + this.readable_stream_ref.deinit(); + return; + } + const resp = this.resp.?; + + stream.value.ensureStillAlive(); + + var response_stream = this.allocator.create(ResponseStream.JSSink) catch unreachable; + response_stream.* = ResponseStream.JSSink{ + .sink = .{ + .res = resp, + .allocator = this.allocator, + .buffer = bun.ByteList{}, + .onFirstWrite = @ptrCast(&handleFirstStreamWrite), + .ctx = this, + .globalThis = globalThis, + }, + }; + var signal = &response_stream.sink.signal; + this.sink = response_stream; + + signal.* = ResponseStream.JSSink.SinkSignal.init(JSValue.zero); + + // explicitly set it to a dead pointer + // we use this memory address to disable signals being sent + signal.clear(); + assert(signal.isDead()); + // we need to render metadata before assignToStream because the stream can call res.end + // and this would auto write an 200 status + if (!this.flags.has_written_status) { + this.renderMetadata(); + } + + // We are already corked! + const assignment_result: JSValue = ResponseStream.JSSink.assignToStream( + globalThis, + stream.value, + response_stream, + @as(**anyopaque, @ptrCast(&signal.ptr)), + ); + + assignment_result.ensureStillAlive(); + + // assert that it was updated + assert(!signal.isDead()); + + if (comptime Environment.allow_assert) { + if (resp.hasResponded()) { + streamLog("responded", .{}); + } + } + + this.flags.aborted = this.flags.aborted or response_stream.sink.aborted; + + if (assignment_result.toError()) |err_value| { + streamLog("returned an error", .{}); + response_stream.detach(); + this.sink = null; + response_stream.sink.destroy(); + return this.handleReject(err_value); + } + + if (resp.hasResponded()) { + streamLog("done", .{}); + response_stream.detach(); + this.sink = null; + response_stream.sink.destroy(); + stream.done(globalThis); + this.readable_stream_ref.deinit(); + this.endStream(this.shouldCloseConnection()); + return; + } + + if (!assignment_result.isEmptyOrUndefinedOrNull()) { + assignment_result.ensureStillAlive(); + // it returns a Promise when it goes through ReadableStreamDefaultReader + if (assignment_result.asAnyPromise()) |promise| { + streamLog("returned a promise", .{}); + this.drainMicrotasks(); + + switch (promise.status(globalThis.vm())) { + .pending => { + streamLog("promise still Pending", .{}); + if (!this.flags.has_written_status) { + response_stream.sink.onFirstWrite = null; + response_stream.sink.ctx = null; + this.renderMetadata(); + } + + // TODO: should this timeout? + this.response_ptr.?.body.value = .{ + .Locked = .{ + .readable = JSC.WebCore.ReadableStream.Strong.init(stream, globalThis), + .global = globalThis, + }, + }; + this.ref(); + assignment_result.then( + globalThis, + this, + onResolveStream, + onRejectStream, + ); + // the response_stream should be GC'd + + }, + .fulfilled => { + streamLog("promise Fulfilled", .{}); + var readable_stream_ref = this.readable_stream_ref; + this.readable_stream_ref = .{}; + defer { + stream.done(globalThis); + readable_stream_ref.deinit(); + } + + this.handleResolveStream(); + }, + .rejected => { + streamLog("promise Rejected", .{}); + var readable_stream_ref = this.readable_stream_ref; + this.readable_stream_ref = .{}; + defer { + stream.cancel(globalThis); + readable_stream_ref.deinit(); + } + this.handleRejectStream(globalThis, promise.result(globalThis.vm())); + }, + } + return; + } else { + // if is not a promise we treat it as Error + streamLog("returned an error", .{}); + response_stream.detach(); + this.sink = null; + response_stream.sink.destroy(); + return this.handleReject(assignment_result); + } + } + + if (this.isAbortedOrEnded()) { + response_stream.detach(); + stream.cancel(globalThis); + defer this.readable_stream_ref.deinit(); + + response_stream.sink.markDone(); + response_stream.sink.onFirstWrite = null; + + response_stream.sink.finalize(); + return; + } + var readable_stream_ref = this.readable_stream_ref; + this.readable_stream_ref = .{}; + defer readable_stream_ref.deinit(); + + const is_in_progress = response_stream.sink.has_backpressure or !(response_stream.sink.wrote == 0 and + response_stream.sink.buffer.len == 0); + + if (!stream.isLocked(globalThis) and !is_in_progress) { + if (JSC.WebCore.ReadableStream.fromJS(stream.value, globalThis)) |comparator| { + if (std.meta.activeTag(comparator.ptr) == std.meta.activeTag(stream.ptr)) { + streamLog("is not locked", .{}); + this.renderMissing(); + return; + } + } + } + + streamLog("is in progress, but did not return a Promise. Finalizing request context", .{}); + response_stream.sink.onFirstWrite = null; + response_stream.sink.ctx = null; + response_stream.detach(); + stream.cancel(globalThis); + response_stream.sink.markDone(); + this.renderMissing(); + } + + const streamLog = Output.scoped(.ReadableStream, false); + + pub fn didUpgradeWebSocket(this: *RequestContext) bool { + return @intFromPtr(this.upgrade_context) == std.math.maxInt(usize); + } + + fn toAsyncWithoutAbortHandler(ctx: *RequestContext, req: *uws.Request, request_object: *Request) void { + request_object.request_context.setRequest(req); + assert(ctx.server != null); + + request_object.ensureURL() catch { + request_object.url = bun.String.empty; + }; + + // we have to clone the request headers here since they will soon belong to a different request + if (!request_object.hasFetchHeaders()) { + request_object.setFetchHeaders(.createFromUWS(req)); + } + + // This object dies after the stack frame is popped + // so we have to clear it in here too + request_object.request_context.detachRequest(); + } + + pub fn toAsync( + ctx: *RequestContext, + req: *uws.Request, + request_object: *Request, + ) void { + ctxLog("toAsync", .{}); + ctx.toAsyncWithoutAbortHandler(req, request_object); + if (comptime debug_mode) { + ctx.pathname = request_object.url.clone(); + } + ctx.setAbortHandler(); + } + + fn endRequestStreamingAndDrain(this: *RequestContext) void { + assert(this.server != null); + + if (this.endRequestStreaming()) { + this.server.?.vm.drainMicrotasks(); + } + } + fn endRequestStreaming(this: *RequestContext) bool { + assert(this.server != null); + + this.request_body_buf.clearAndFree(bun.default_allocator); + + // if we cannot, we have to reject pending promises + // first, we reject the request body promise + if (this.request_body) |body| { + // User called .blob(), .json(), text(), or .arrayBuffer() on the Request object + // but we received nothing or the connection was aborted + if (body.value == .Locked) { + body.value.toErrorInstance(.{ .AbortReason = .ConnectionClosed }, this.server.?.globalThis); + return true; + } + } + return false; + } + fn detachResponse(this: *RequestContext) void { + this.request_body_buf.clearAndFree(bun.default_allocator); + + if (this.resp) |resp| { + this.resp = null; + + if (this.flags.is_waiting_for_request_body) { + this.flags.is_waiting_for_request_body = false; + resp.clearOnData(); + } + if (this.flags.has_abort_handler) { + resp.clearAborted(); + this.flags.has_abort_handler = false; + } + if (this.flags.has_timeout_handler) { + resp.clearTimeout(); + this.flags.has_timeout_handler = false; + } + } + } + + pub fn isAbortedOrEnded(this: *const RequestContext) bool { + // resp == null or aborted or server.stop(true) + return this.resp == null or this.flags.aborted or this.server == null or this.server.?.flags.terminated; + } + const HeaderResponseSizePair = struct { this: *RequestContext, size: usize }; + pub fn doRenderHeadResponseAfterS3SizeResolved(pair: *HeaderResponseSizePair) void { + var this = pair.this; + this.renderMetadata(); + + if (this.resp) |resp| { + resp.writeHeaderInt("content-length", pair.size); + } + this.endWithoutBody(this.shouldCloseConnection()); + this.deref(); + } + pub fn onS3SizeResolved(result: S3.S3StatResult, this: *RequestContext) void { + defer { + this.deref(); + } + if (this.resp) |resp| { + var pair = HeaderResponseSizePair{ .this = this, .size = switch (result) { + .failure, .not_found => 0, + .success => |stat| stat.size, + } }; + resp.runCorkedWithType(*HeaderResponseSizePair, doRenderHeadResponseAfterS3SizeResolved, &pair); + } + } + const HeaderResponsePair = struct { this: *RequestContext, response: *JSC.WebCore.Response }; + + fn doRenderHeadResponse(pair: *HeaderResponsePair) void { + var this = pair.this; + var response = pair.response; + if (this.resp == null) { + return; + } + // we will render the content-length header later manually so we set this to false + this.flags.needs_content_length = false; + // Always this.renderMetadata() before sending the content-length or transfer-encoding header so status is sent first + + const resp = this.resp.?; + this.response_ptr = response; + const server = this.server orelse { + // server detached? + this.renderMetadata(); + resp.writeHeaderInt("content-length", 0); + this.endWithoutBody(this.shouldCloseConnection()); + return; + }; + const globalThis = server.globalThis; + if (response.getFetchHeaders()) |headers| { + // first respect the headers + if (headers.fastGet(.TransferEncoding)) |transfer_encoding| { + const transfer_encoding_str = transfer_encoding.toSlice(server.allocator); + defer transfer_encoding_str.deinit(); + this.renderMetadata(); + resp.writeHeader("transfer-encoding", transfer_encoding_str.slice()); + this.endWithoutBody(this.shouldCloseConnection()); + + return; + } + if (headers.fastGet(.ContentLength)) |content_length| { + const content_length_str = content_length.toSlice(server.allocator); + defer content_length_str.deinit(); + this.renderMetadata(); + + const len = std.fmt.parseInt(usize, content_length_str.slice(), 10) catch 0; + resp.writeHeaderInt("content-length", len); + this.endWithoutBody(this.shouldCloseConnection()); + return; + } + } + // not content-length or transfer-encoding so we need to respect the body + response.body.value.toBlobIfPossible(); + switch (response.body.value) { + .InternalBlob, .WTFStringImpl => { + var blob = response.body.value.useAsAnyBlobAllowNonUTF8String(); + defer blob.detach(); + const size = blob.size(); + this.renderMetadata(); + + if (size == Blob.max_size) { + resp.writeHeaderInt("content-length", 0); + } else { + resp.writeHeaderInt("content-length", size); + } + this.endWithoutBody(this.shouldCloseConnection()); + }, + + .Blob => |*blob| { + if (blob.isS3()) { + // we need to read the size asynchronously + // in this case should always be a redirect so should not hit this path, but in case we change it in the future lets handle it + this.ref(); + + const credentials = blob.store.?.data.s3.getCredentials(); + const path = blob.store.?.data.s3.path(); + const env = globalThis.bunVM().transpiler.env; + + S3.stat(credentials, path, @ptrCast(&onS3SizeResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + + return; + } + this.renderMetadata(); + + blob.resolveSize(); + if (blob.size == Blob.max_size) { + resp.writeHeaderInt("content-length", 0); + } else { + resp.writeHeaderInt("content-length", blob.size); + } + this.endWithoutBody(this.shouldCloseConnection()); + }, + .Locked => { + this.renderMetadata(); + resp.writeHeader("transfer-encoding", "chunked"); + this.endWithoutBody(this.shouldCloseConnection()); + }, + .Used, .Null, .Empty, .Error => { + this.renderMetadata(); + resp.writeHeaderInt("content-length", 0); + this.endWithoutBody(this.shouldCloseConnection()); + }, + } + } + + // Each HTTP request or TCP socket connection is effectively a "task". + // + // However, unlike the regular task queue, we don't drain the microtask + // queue at the end. + // + // Instead, we drain it multiple times, at the points that would + // otherwise "halt" the Response from being rendered. + // + // - If you return a Promise, we drain the microtask queue once + // - If you return a streaming Response, we drain the microtask queue (possibly the 2nd time this task!) + pub fn onResponse( + ctx: *RequestContext, + this: *ThisServer, + request_value: JSValue, + response_value: JSValue, + ) void { + request_value.ensureStillAlive(); + response_value.ensureStillAlive(); + ctx.drainMicrotasks(); + + if (ctx.isAbortedOrEnded()) { + return; + } + // if you return a Response object or a Promise + // but you upgraded the connection to a WebSocket + // just ignore the Response object. It doesn't do anything. + // it's better to do that than to throw an error + if (ctx.didUpgradeWebSocket()) { + return; + } + + if (response_value.isEmptyOrUndefinedOrNull()) { + ctx.renderMissingInvalidResponse(response_value); + return; + } + + if (response_value.toError()) |err_value| { + ctx.runErrorHandler(err_value); + return; + } + + if (response_value.as(JSC.WebCore.Response)) |response| { + ctx.response_jsvalue = response_value; + ctx.response_jsvalue.ensureStillAlive(); + ctx.flags.response_protected = false; + if (ctx.method == .HEAD) { + if (ctx.resp) |resp| { + var pair = HeaderResponsePair{ .this = ctx, .response = response }; + resp.runCorkedWithType(*HeaderResponsePair, doRenderHeadResponse, &pair); + } + return; + } else { + response.body.value.toBlobIfPossible(); + + switch (response.body.value) { + .Blob => |*blob| { + if (blob.needsToReadFile()) { + response_value.protect(); + ctx.flags.response_protected = true; + } + }, + .Locked => { + response_value.protect(); + ctx.flags.response_protected = true; + }, + else => {}, + } + ctx.render(response); + } + return; + } + + var vm = this.vm; + + if (response_value.asAnyPromise()) |promise| { + // If we immediately have the value available, we can skip the extra event loop tick + switch (promise.unwrap(vm.global.vm(), .mark_handled)) { + .pending => { + ctx.ref(); + response_value.then(this.globalThis, ctx, RequestContext.onResolve, RequestContext.onReject); + return; + }, + .fulfilled => |fulfilled_value| { + // if you return a Response object or a Promise + // but you upgraded the connection to a WebSocket + // just ignore the Response object. It doesn't do anything. + // it's better to do that than to throw an error + if (ctx.didUpgradeWebSocket()) { + return; + } + + if (fulfilled_value.isEmptyOrUndefinedOrNull()) { + ctx.renderMissingInvalidResponse(fulfilled_value); + return; + } + var response = fulfilled_value.as(JSC.WebCore.Response) orelse { + ctx.renderMissingInvalidResponse(fulfilled_value); + return; + }; + + ctx.response_jsvalue = fulfilled_value; + ctx.response_jsvalue.ensureStillAlive(); + ctx.flags.response_protected = false; + ctx.response_ptr = response; + if (ctx.method == .HEAD) { + if (ctx.resp) |resp| { + var pair = HeaderResponsePair{ .this = ctx, .response = response }; + resp.runCorkedWithType(*HeaderResponsePair, doRenderHeadResponse, &pair); + } + return; + } + response.body.value.toBlobIfPossible(); + switch (response.body.value) { + .Blob => |*blob| { + if (blob.needsToReadFile()) { + fulfilled_value.protect(); + ctx.flags.response_protected = true; + } + }, + .Locked => { + fulfilled_value.protect(); + ctx.flags.response_protected = true; + }, + else => {}, + } + ctx.render(response); + return; + }, + .rejected => |err| { + ctx.handleReject(err); + return; + }, + } + } + } + + pub fn handleResolveStream(req: *RequestContext) void { + streamLog("handleResolveStream", .{}); + + var wrote_anything = false; + if (req.sink) |wrapper| { + req.flags.aborted = req.flags.aborted or wrapper.sink.aborted; + wrote_anything = wrapper.sink.wrote > 0; + + wrapper.sink.finalize(); + wrapper.detach(); + req.sink = null; + wrapper.sink.destroy(); + } + + if (req.response_ptr) |resp| { + assert(req.server != null); + + if (resp.body.value == .Locked) { + const global = resp.body.value.Locked.global; + if (resp.body.value.Locked.readable.get(global)) |stream| { + stream.done(global); + } + resp.body.value.Locked.readable.deinit(); + resp.body.value = .{ .Used = {} }; + } + } + + if (req.isAbortedOrEnded()) { + return; + } + + streamLog("onResolve({any})", .{wrote_anything}); + if (!req.flags.has_written_status) { + req.renderMetadata(); + } + req.endStream(req.shouldCloseConnection()); + } + + pub fn onResolveStream(_: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + streamLog("onResolveStream", .{}); + var args = callframe.arguments_old(2); + var req: *@This() = args.ptr[args.len - 1].asPromisePtr(@This()); + defer req.deref(); + req.handleResolveStream(); + return JSValue.jsUndefined(); + } + pub fn onRejectStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + streamLog("onRejectStream", .{}); + const args = callframe.arguments_old(2); + var req = args.ptr[args.len - 1].asPromisePtr(@This()); + const err = args.ptr[0]; + defer req.deref(); + + req.handleRejectStream(globalThis, err); + return JSValue.jsUndefined(); + } + + pub fn handleRejectStream(req: *@This(), globalThis: *JSC.JSGlobalObject, err: JSValue) void { + streamLog("handleRejectStream", .{}); + + if (req.sink) |wrapper| { + wrapper.sink.pending_flush = null; + wrapper.sink.done = true; + req.flags.aborted = req.flags.aborted or wrapper.sink.aborted; + wrapper.sink.finalize(); + wrapper.detach(); + req.sink = null; + wrapper.sink.destroy(); + } + + if (req.response_ptr) |resp| { + if (resp.body.value == .Locked) { + if (resp.body.value.Locked.readable.get(globalThis)) |stream| { + stream.done(globalThis); + } + resp.body.value.Locked.readable.deinit(); + resp.body.value = .{ .Used = {} }; + } + } + + // aborted so call finalizeForAbort + if (req.isAbortedOrEnded()) { + return; + } + + streamLog("onReject()", .{}); + + if (!req.flags.has_written_status) { + req.renderMetadata(); + } + + if (comptime debug_mode) { + if (req.server) |server| { + if (!err.isEmptyOrUndefinedOrNull()) { + var exception_list: std.ArrayList(Api.JsException) = std.ArrayList(Api.JsException).init(req.allocator); + defer exception_list.deinit(); + server.vm.runErrorHandler(err, &exception_list); + } + } + } + req.endStream(req.shouldCloseConnection()); + } + + pub fn doRenderWithBody(this: *RequestContext, value: *JSC.WebCore.Body.Value) void { + this.drainMicrotasks(); + + // If a ReadableStream can trivially be converted to a Blob, do so. + // If it's a WTFStringImpl and it cannot be used as a UTF-8 string, convert it to a Blob. + value.toBlobIfPossible(); + const globalThis = this.server.?.globalThis; + switch (value.*) { + .Error => |*err_ref| { + _ = value.use(); + if (this.isAbortedOrEnded()) { + return; + } + this.runErrorHandler(err_ref.toJS(globalThis)); + return; + }, + // .InlineBlob, + .WTFStringImpl, + .InternalBlob, + .Blob, + => { + // toBlobIfPossible checks for WTFString needing a conversion. + this.blob = value.useAsAnyBlobAllowNonUTF8String(); + this.renderWithBlobFromBodyValue(); + return; + }, + .Locked => |*lock| { + if (this.isAbortedOrEnded()) { + return; + } + + if (lock.readable.get(globalThis)) |stream_| { + const stream: JSC.WebCore.ReadableStream = stream_; + // we hold the stream alive until we're done with it + this.readable_stream_ref = lock.readable; + value.* = .{ .Used = {} }; + + if (stream.isLocked(globalThis)) { + streamLog("was locked but it shouldn't be", .{}); + var err = JSC.SystemError{ + .code = bun.String.static(@tagName(JSC.Node.ErrorCode.ERR_STREAM_CANNOT_PIPE)), + .message = bun.String.static("Stream already used, please create a new one"), + }; + stream.value.unprotect(); + this.runErrorHandler(err.toErrorInstance(globalThis)); + return; + } + + switch (stream.ptr) { + .Invalid => { + this.readable_stream_ref.deinit(); + }, + // toBlobIfPossible will typically convert .Blob streams, or .File streams into a Blob object, but cannot always. + .Blob, + .File, + // These are the common scenario: + .JavaScript, + .Direct, + => { + if (this.resp) |resp| { + var pair = StreamPair{ .stream = stream, .this = this }; + resp.runCorkedWithType(*StreamPair, doRenderStream, &pair); + } + return; + }, + + .Bytes => |byte_stream| { + assert(byte_stream.pipe.ctx == null); + assert(this.byte_stream == null); + if (this.resp == null) { + // we don't have a response, so we can discard the stream + stream.done(globalThis); + this.readable_stream_ref.deinit(); + return; + } + const resp = this.resp.?; + // If we've received the complete body by the time this function is called + // we can avoid streaming it and just send it all at once. + if (byte_stream.has_received_last_chunk) { + this.blob = .fromArrayList(byte_stream.drain().listManaged(bun.default_allocator)); + this.readable_stream_ref.deinit(); + this.doRenderBlob(); + return; + } + this.ref(); + byte_stream.pipe = JSC.WebCore.Pipe.Wrap(@This(), onPipe).init(this); + this.readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(stream, globalThis); + + this.byte_stream = byte_stream; + this.response_buf_owned = byte_stream.drain().list(); + + // we don't set size here because even if we have a hint + // uWebSockets won't let us partially write streaming content + this.blob.detach(); + + // if we've received metadata and part of the body, send everything we can and drain + if (this.response_buf_owned.items.len > 0) { + resp.runCorkedWithType(*RequestContext, drainResponseBufferAndMetadata, this); + } else { + // if we only have metadata to send, send it now + resp.runCorkedWithType(*RequestContext, renderMetadata, this); + } + return; + }, + } + } + + if (lock.onReceiveValue != null or lock.task != null) { + // someone else is waiting for the stream or waiting for `onStartStreaming` + const readable = value.toReadableStream(globalThis); + readable.ensureStillAlive(); + this.doRenderWithBody(value); + return; + } + + // when there's no stream, we need to + lock.onReceiveValue = doRenderWithBodyLocked; + lock.task = this; + + return; + }, + else => {}, + } + + this.doRenderBlob(); + } + + pub fn onPipe(this: *RequestContext, stream: JSC.WebCore.streams.Result, allocator: std.mem.Allocator) void { + const stream_needs_deinit = stream == .owned or stream == .owned_and_done; + const is_done = stream.isDone(); + defer { + if (is_done) this.deref(); + if (stream_needs_deinit) { + if (is_done) { + stream.owned_and_done.listManaged(allocator).deinit(); + } else { + stream.owned.listManaged(allocator).deinit(); + } + } + } + + if (this.isAbortedOrEnded()) { + return; + } + const resp = this.resp.?; + + const chunk = stream.slice(); + // on failure, it will continue to allocate + // 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) == .want_more) { + if (is_done) { + this.endStream(this.shouldCloseConnection()); + } + } else { + // when it's the last one, we just want to know if it's done + if (is_done) { + this.flags.has_marked_pending = true; + resp.onWritable(*RequestContext, onWritableResponseBuffer, this); + } + } + } + + pub fn doRenderBlob(this: *RequestContext) void { + // We are not corked + // The body is small + // Faster to do the memcpy than to do the two network calls + // We are not streaming + // This is an important performance optimization + if (this.flags.has_abort_handler and this.blob.fastSize() < 16384 - 1024) { + if (this.resp) |resp| { + resp.runCorkedWithType(*RequestContext, doRenderBlobCorked, this); + } + } else { + this.doRenderBlobCorked(); + } + } + + pub fn doRenderBlobCorked(this: *RequestContext) void { + this.renderMetadata(); + this.renderBytes(); + } + + pub fn doRender(this: *RequestContext) void { + ctxLog("doRender", .{}); + + if (this.isAbortedOrEnded()) { + return; + } + var response = this.response_ptr.?; + this.doRenderWithBody(&response.body.value); + } + + pub fn renderProductionError(this: *RequestContext, status: u16) void { + if (this.resp) |resp| { + switch (status) { + 404 => { + if (!this.flags.has_written_status) { + resp.writeStatus("404 Not Found"); + this.flags.has_written_status = true; + } + this.endWithoutBody(this.shouldCloseConnection()); + }, + else => { + if (!this.flags.has_written_status) { + resp.writeStatus("500 Internal Server Error"); + resp.writeHeader("content-type", "text/plain"); + this.flags.has_written_status = true; + } + + this.end("Something went wrong!", this.shouldCloseConnection()); + }, + } + } + } + + pub fn runErrorHandler( + this: *RequestContext, + value: JSC.JSValue, + ) void { + runErrorHandlerWithStatusCode(this, value, 500); + } + + const PathnameFormatter = struct { + ctx: *RequestContext, + + pub fn format(formatter: @This(), comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void { + var this = formatter.ctx; + + if (!this.pathname.isEmpty()) { + try this.pathname.format(fmt, opts, writer); + return; + } + + if (!this.flags.has_abort_handler) { + if (this.req) |req| { + try writer.writeAll(req.url()); + return; + } + } + + try writer.writeAll("/"); + } + }; + + fn ensurePathname(this: *RequestContext) PathnameFormatter { + return .{ .ctx = this }; + } + + pub inline fn shouldCloseConnection(this: *const RequestContext) bool { + if (this.resp) |resp| { + return resp.shouldCloseConnection(); + } + return false; + } + + fn finishRunningErrorHandler(this: *RequestContext, value: JSC.JSValue, status: u16) void { + if (this.server == null) return this.renderProductionError(status); + var vm: *JSC.VirtualMachine = this.server.?.vm; + const globalThis = this.server.?.globalThis; + if (comptime debug_mode) { + var exception_list: std.ArrayList(Api.JsException) = std.ArrayList(Api.JsException).init(this.allocator); + defer exception_list.deinit(); + const prev_exception_list = vm.onUnhandledRejectionExceptionList; + vm.onUnhandledRejectionExceptionList = &exception_list; + vm.onUnhandledRejection(vm, globalThis, value); + vm.onUnhandledRejectionExceptionList = prev_exception_list; + + this.renderDefaultError( + vm.log, + error.ExceptionOcurred, + exception_list.toOwnedSlice() catch @panic("TODO"), + "{s} - {} failed", + .{ @as(string, @tagName(this.method)), this.ensurePathname() }, + ); + } else { + if (status != 404) { + vm.onUnhandledRejection(vm, globalThis, value); + } + this.renderProductionError(status); + } + + vm.log.reset(); + } + + pub fn runErrorHandlerWithStatusCodeDontCheckResponded( + this: *RequestContext, + value: JSC.JSValue, + status: u16, + ) void { + JSC.markBinding(@src()); + if (this.server) |server| { + if (server.config.onError != .zero and !this.flags.has_called_error_handler) { + this.flags.has_called_error_handler = true; + const result = server.config.onError.call( + server.globalThis, + server.js_value.get() orelse .undefined, + &.{value}, + ) catch |err| server.globalThis.takeException(err); + defer result.ensureStillAlive(); + if (!result.isEmptyOrUndefinedOrNull()) { + if (result.toError()) |err| { + this.finishRunningErrorHandler(err, status); + return; + } else if (result.asAnyPromise()) |promise| { + this.processOnErrorPromise(result, promise, value, status); + return; + } else if (result.as(Response)) |response| { + this.render(response); + return; + } + } + } + } + + this.finishRunningErrorHandler(value, status); + } + + fn processOnErrorPromise( + ctx: *RequestContext, + promise_js: JSC.JSValue, + promise: JSC.AnyPromise, + value: JSC.JSValue, + status: u16, + ) void { + assert(ctx.server != null); + var vm = ctx.server.?.vm; + + switch (promise.unwrap(vm.global.vm(), .mark_handled)) { + .pending => { + ctx.flags.is_error_promise_pending = true; + ctx.ref(); + promise_js.then( + ctx.server.?.globalThis, + ctx, + RequestContext.onResolve, + RequestContext.onReject, + ); + }, + .fulfilled => |fulfilled_value| { + // if you return a Response object or a Promise + // but you upgraded the connection to a WebSocket + // just ignore the Response object. It doesn't do anything. + // it's better to do that than to throw an error + if (ctx.didUpgradeWebSocket()) { + return; + } + + var response = fulfilled_value.as(JSC.WebCore.Response) orelse { + ctx.finishRunningErrorHandler(value, status); + return; + }; + + ctx.response_jsvalue = fulfilled_value; + ctx.response_jsvalue.ensureStillAlive(); + ctx.flags.response_protected = false; + ctx.response_ptr = response; + + response.body.value.toBlobIfPossible(); + switch (response.body.value) { + .Blob => |*blob| { + if (blob.needsToReadFile()) { + fulfilled_value.protect(); + ctx.flags.response_protected = true; + } + }, + .Locked => { + fulfilled_value.protect(); + ctx.flags.response_protected = true; + }, + else => {}, + } + ctx.render(response); + return; + }, + .rejected => |err| { + ctx.finishRunningErrorHandler(err, status); + return; + }, + } + } + + pub fn runErrorHandlerWithStatusCode( + this: *RequestContext, + value: JSC.JSValue, + status: u16, + ) void { + JSC.markBinding(@src()); + if (this.resp == null or this.resp.?.hasResponded()) return; + + runErrorHandlerWithStatusCodeDontCheckResponded(this, value, status); + } + + pub fn renderMetadata(this: *RequestContext) void { + if (this.resp == null) return; + const resp = this.resp.?; + + var response: *JSC.WebCore.Response = this.response_ptr.?; + var status = response.statusCode(); + var needs_content_range = this.flags.needs_content_range and this.sendfile.remain < this.blob.size(); + + const size = if (needs_content_range) + this.sendfile.remain + else + this.blob.size(); + + status = if (status == 200 and size == 0 and !this.blob.isDetached()) + 204 + else + status; + + const content_type, const needs_content_type, const content_type_needs_free = getContentType( + response.init.headers, + &this.blob, + this.allocator, + ); + defer if (content_type_needs_free) content_type.deinit(this.allocator); + var has_content_disposition = false; + var has_content_range = false; + if (response.init.headers) |headers_| { + has_content_disposition = headers_.fastHas(.ContentDisposition); + has_content_range = headers_.fastHas(.ContentRange); + needs_content_range = needs_content_range and has_content_range; + if (needs_content_range) { + status = 206; + } + + this.doWriteStatus(status); + this.doWriteHeaders(headers_); + response.init.headers = null; + headers_.deref(); + } else if (needs_content_range) { + status = 206; + this.doWriteStatus(status); + } else { + this.doWriteStatus(status); + } + + if (this.cookies) |cookies| { + this.cookies = null; + defer cookies.deref(); + cookies.write(this.server.?.globalThis, ssl_enabled, @ptrCast(this.resp.?)); + } + + if (needs_content_type and + // do not insert the content type if it is the fallback value + // we may not know the content-type when streaming + (!this.blob.isDetached() or content_type.value.ptr != MimeType.other.value.ptr)) + { + resp.writeHeader("content-type", content_type.value); + } + + // automatically include the filename when: + // 1. Bun.file("foo") + // 2. The content-disposition header is not present + if (!has_content_disposition and content_type.category.autosetFilename()) { + if (this.blob.getFileName()) |filename| { + const basename = std.fs.path.basename(filename); + if (basename.len > 0) { + var filename_buf: [1024]u8 = undefined; + + resp.writeHeader( + "content-disposition", + std.fmt.bufPrint(&filename_buf, "filename=\"{s}\"", .{basename[0..@min(basename.len, 1024 - 32)]}) catch "", + ); + } + } + } + + if (this.flags.needs_content_length) { + resp.writeHeaderInt("content-length", size); + this.flags.needs_content_length = false; + } + + if (needs_content_range and !has_content_range) { + var content_range_buf: [1024]u8 = undefined; + + resp.writeHeader( + "content-range", + std.fmt.bufPrint( + &content_range_buf, + // we omit the full size of the Blob because it could + // change between requests and this potentially leaks + // PII undesirably + "bytes {d}-{d}/*", + .{ this.sendfile.offset, this.sendfile.offset + (this.sendfile.remain -| 1) }, + ) catch "bytes */*", + ); + this.flags.needs_content_range = false; + } + } + + fn doWriteStatus(this: *RequestContext, status: u16) void { + assert(!this.flags.has_written_status); + this.flags.has_written_status = true; + + writeStatus(ssl_enabled, this.resp, status); + } + + fn doWriteHeaders(this: *RequestContext, headers: *WebCore.FetchHeaders) void { + writeHeaders(headers, ssl_enabled, this.resp); + } + + pub fn renderBytes(this: *RequestContext) void { + // copy it to stack memory to prevent aliasing issues in release builds + const blob = this.blob; + const bytes = blob.slice(); + if (this.resp) |resp| { + if (!resp.tryEnd( + bytes, + bytes.len, + this.shouldCloseConnection(), + )) { + this.flags.has_marked_pending = true; + resp.onWritable(*RequestContext, onWritableBytes, this); + return; + } + } + this.detachResponse(); + this.endRequestStreamingAndDrain(); + this.deref(); + } + + pub fn render(this: *RequestContext, response: *JSC.WebCore.Response) void { + ctxLog("render", .{}); + this.response_ptr = response; + + this.doRender(); + } + + pub fn onBufferedBodyChunk(this: *RequestContext, resp: *App.Response, chunk: []const u8, last: bool) void { + ctxLog("onBufferedBodyChunk {} {}", .{ chunk.len, last }); + + assert(this.resp == resp); + + this.flags.is_waiting_for_request_body = last == false; + if (this.isAbortedOrEnded() or this.flags.has_marked_complete) return; + if (!last and chunk.len == 0) { + // Sometimes, we get back an empty chunk + // We have to ignore those chunks unless it's the last one + return; + } + const vm = this.server.?.vm; + const globalThis = this.server.?.globalThis; + + // After the user does request.body, + // if they then do .text(), .arrayBuffer(), etc + // we can no longer hold the strong reference from the body value ref. + if (this.request_body_readable_stream_ref.get(globalThis)) |readable| { + assert(this.request_body_buf.items.len == 0); + vm.eventLoop().enter(); + defer vm.eventLoop().exit(); + + if (!last) { + readable.ptr.Bytes.onData( + .{ + .temporary = bun.ByteList.initConst(chunk), + }, + bun.default_allocator, + ); + } else { + var strong = this.request_body_readable_stream_ref; + this.request_body_readable_stream_ref = .{}; + defer strong.deinit(); + if (this.request_body) |request_body| { + _ = request_body.unref(); + this.request_body = null; + } + + readable.value.ensureStillAlive(); + readable.ptr.Bytes.onData( + .{ + .temporary_and_done = bun.ByteList.initConst(chunk), + }, + bun.default_allocator, + ); + } + + return; + } + + // This is the start of a task, so it's a good time to drain + if (this.request_body != null) { + var body = this.request_body.?; + + if (last) { + var bytes = &this.request_body_buf; + + var old = body.value; + + const total = bytes.items.len + chunk.len; + getter: { + // if (total <= JSC.WebCore.InlineBlob.available_bytes) { + // if (total == 0) { + // body.value = .{ .Empty = {} }; + // break :getter; + // } + + // body.value = .{ .InlineBlob = JSC.WebCore.InlineBlob.concat(bytes.items, chunk) }; + // this.request_body_buf.clearAndFree(this.allocator); + // } else { + bytes.ensureTotalCapacityPrecise(this.allocator, total) catch |err| { + this.request_body_buf.clearAndFree(this.allocator); + body.value.toError(err, globalThis); + break :getter; + }; + + const prev_len = bytes.items.len; + bytes.items.len = total; + var slice = bytes.items[prev_len..]; + @memcpy(slice[0..chunk.len], chunk); + body.value = .{ + .InternalBlob = .{ + .bytes = bytes.toManaged(this.allocator), + }, + }; + // } + } + this.request_body_buf = .{}; + + if (old == .Locked) { + var loop = vm.eventLoop(); + loop.enter(); + defer loop.exit(); + + old.resolve(&body.value, globalThis, null); + } + return; + } + + if (this.request_body_buf.capacity == 0) { + this.request_body_buf.ensureTotalCapacityPrecise(this.allocator, @min(this.request_body_content_len, max_request_body_preallocate_length)) catch @panic("Out of memory while allocating request body buffer"); + } + this.request_body_buf.appendSlice(this.allocator, chunk) catch @panic("Out of memory while allocating request body"); + } + } + + pub fn onStartStreamingRequestBody(this: *RequestContext) JSC.WebCore.DrainResult { + ctxLog("onStartStreamingRequestBody", .{}); + if (this.isAbortedOrEnded()) { + return JSC.WebCore.DrainResult{ + .aborted = {}, + }; + } + + // This means we have received part of the body but not the whole thing + if (this.request_body_buf.items.len > 0) { + var emptied = this.request_body_buf; + this.request_body_buf = .{}; + return .{ + .owned = .{ + .list = emptied.toManaged(this.allocator), + .size_hint = if (emptied.capacity < max_request_body_preallocate_length) + emptied.capacity + else + 0, + }, + }; + } + + return .{ + .estimated_size = this.request_body_content_len, + }; + } + const max_request_body_preallocate_length = 1024 * 256; + pub fn onStartBuffering(this: *RequestContext) void { + if (this.server) |server| { + ctxLog("onStartBuffering", .{}); + // TODO: check if is someone calling onStartBuffering other than onStartBufferingCallback + // if is not, this should be removed and only keep protect + setAbortHandler + if (this.flags.is_transfer_encoding == false and this.request_body_content_len == 0) { + // no content-length or 0 content-length + // no transfer-encoding + if (this.request_body != null) { + var body = this.request_body.?; + var old = body.value; + old.Locked.onReceiveValue = null; + var new_body: WebCore.Body.Value = .{ .Null = {} }; + old.resolve(&new_body, server.globalThis, null); + body.value = new_body; + } + } + } + } + + pub fn onRequestBodyReadableStreamAvailable(ptr: *anyopaque, globalThis: *JSC.JSGlobalObject, readable: JSC.WebCore.ReadableStream) void { + var this = bun.cast(*RequestContext, ptr); + bun.debugAssert(this.request_body_readable_stream_ref.held.impl == null); + this.request_body_readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(readable, globalThis); + } + + pub fn onStartBufferingCallback(this: *anyopaque) void { + onStartBuffering(bun.cast(*RequestContext, this)); + } + + pub fn onStartStreamingRequestBodyCallback(this: *anyopaque) JSC.WebCore.DrainResult { + return onStartStreamingRequestBody(bun.cast(*RequestContext, this)); + } + + pub fn getRemoteSocketInfo(this: *RequestContext) ?uws.SocketAddress { + return (this.resp orelse return null).getRemoteSocketInfo(); + } + + pub fn setTimeout(this: *RequestContext, seconds: c_uint) bool { + if (this.resp) |resp| { + resp.timeout(@min(seconds, 255)); + if (seconds > 0) { + + // we only set the timeout callback if we wanna the timeout event to be triggered + // the connection will be closed so the abort handler will be called after the timeout + if (this.request_weakref.get()) |req| { + if (req.internal_event_callback.hasCallback()) { + this.setTimeoutHandler(); + } + } + } else { + // if the timeout is 0, we don't need to trigger the timeout event + resp.clearTimeout(); + } + return true; + } + return false; + } + + comptime { + const export_prefix = "Bun__HTTPRequestContext" ++ (if (debug_mode) "Debug" else "") ++ (if (ThisServer.ssl_enabled) "TLS" else ""); + if (bun.Environment.export_cpp_apis) { + @export(&JSC.toJSHostFn(onResolve), .{ .name = export_prefix ++ "__onResolve" }); + @export(&JSC.toJSHostFn(onReject), .{ .name = export_prefix ++ "__onReject" }); + @export(&JSC.toJSHostFn(onResolveStream), .{ .name = export_prefix ++ "__onResolveStream" }); + @export(&JSC.toJSHostFn(onRejectStream), .{ .name = export_prefix ++ "__onRejectStream" }); + } + } + }; +} + +const SendfileContext = struct { + fd: bun.FileDescriptor, + socket_fd: bun.FileDescriptor = bun.invalid_fd, + remain: Blob.SizeType = 0, + offset: Blob.SizeType = 0, + has_listener: bool = false, + has_set_on_writable: bool = false, + auto_close: bool = false, +}; + +fn NewFlags(comptime debug_mode: bool) type { + return packed struct(u16) { + has_marked_complete: bool = false, + has_marked_pending: bool = false, + has_abort_handler: bool = false, + has_timeout_handler: bool = false, + has_sendfile_ctx: bool = false, + has_called_error_handler: bool = false, + needs_content_length: bool = false, + needs_content_range: bool = false, + /// Used to avoid looking at the uws.Request struct after it's been freed + is_transfer_encoding: bool = false, + + /// Used to identify if request can be safely deinitialized + is_waiting_for_request_body: bool = false, + /// Used in renderMissing in debug mode to show the user an HTML page + /// Used to avoid looking at the uws.Request struct after it's been freed + is_web_browser_navigation: if (debug_mode) bool else void = if (debug_mode) false, + has_written_status: bool = false, + response_protected: bool = false, + aborted: bool = false, + has_finalized: bun.DebugOnly(bool) = if (Environment.isDebug) false, + + is_error_promise_pending: bool = false, + + _padding: PaddingInt = 0, + + const PaddingInt = brk: { + var size: usize = 2; + if (Environment.isDebug) { + size -= 1; + } + + if (debug_mode) { + size -= 1; + } + + break :brk std.meta.Int(.unsigned, size); + }; + }; +} + +fn getContentType(headers: ?*WebCore.FetchHeaders, blob: *const WebCore.Blob.Any, allocator: std.mem.Allocator) struct { MimeType, bool, bool } { + var needs_content_type = true; + var content_type_needs_free = false; + + const content_type: MimeType = brk: { + if (headers) |headers_| { + if (headers_.fastGet(.ContentType)) |content| { + needs_content_type = false; + + var content_slice = content.toSlice(allocator); + defer content_slice.deinit(); + + const content_type_allocator = if (content_slice.allocator.isNull()) null else allocator; + break :brk MimeType.init(content_slice.slice(), content_type_allocator, &content_type_needs_free); + } + } + + break :brk if (blob.contentType().len > 0) + MimeType.byName(blob.contentType()) + else if (MimeType.sniff(blob.slice())) |content| + content + else if (blob.wasString()) + MimeType.text + // TODO: should we get the mime type off of the Blob.Store if it exists? + // A little wary of doing this right now due to causing some breaking change + else + MimeType.other; + }; + + return .{ content_type, needs_content_type, content_type_needs_free }; +} + +const welcome_page_html_gz = @embedFile("../welcome-page.html.gz"); + +fn writeHeaders( + headers: *WebCore.FetchHeaders, + comptime ssl: bool, + resp_ptr: ?*uws.NewApp(ssl).Response, +) void { + ctxLog("writeHeaders", .{}); + headers.fastRemove(.ContentLength); + headers.fastRemove(.TransferEncoding); + if (resp_ptr) |resp| { + headers.toUWSResponse(ssl, resp); + } +} + +const WebCore = JSC.WebCore; +const bun = @import("bun"); +const uws = bun.uws; +const std = @import("std"); +const Environment = bun.Environment; +const JSC = bun.JSC; +const Request = JSC.WebCore.Request; +const Response = JSC.WebCore.Response; +const FetchHeaders = JSC.WebCore.FetchHeaders; +const Body = JSC.WebCore.Body; +const Blob = JSC.WebCore.Blob; +const MimeType = bun.http.MimeType; +const HTTP = bun.http; +const Output = bun.Output; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const String = bun.String; +const JSError = bun.JSError; +const linux = std.os.linux; +const S3 = bun.S3; +const logger = bun.logger; +const assert = bun.assert; +const ctxLog = Output.scoped(.RequestContext, false); +const Api = bun.Schema.Api; +const string = []const u8; +const AnyRequestContext = JSC.API.AnyRequestContext; +const VirtualMachine = JSC.VirtualMachine; +const writeStatus = @import("../server.zig").writeStatus; +const Fallback = @import("../../../runtime.zig").Fallback; diff --git a/src/bun.js/api/server/SSLConfig.zig b/src/bun.js/api/server/SSLConfig.zig new file mode 100644 index 0000000000..5ed7eecbdf --- /dev/null +++ b/src/bun.js/api/server/SSLConfig.zig @@ -0,0 +1,620 @@ +const SSLConfig = @This(); + +requires_custom_request_ctx: bool = false, +server_name: [*c]const u8 = null, + +key_file_name: [*c]const u8 = null, +cert_file_name: [*c]const u8 = null, + +ca_file_name: [*c]const u8 = null, +dh_params_file_name: [*c]const u8 = null, + +passphrase: [*c]const u8 = null, +low_memory_mode: bool = false, + +key: ?[][*c]const u8 = null, +key_count: u32 = 0, + +cert: ?[][*c]const u8 = null, +cert_count: u32 = 0, + +ca: ?[][*c]const u8 = null, +ca_count: u32 = 0, + +secure_options: u32 = 0, +request_cert: i32 = 0, +reject_unauthorized: i32 = 0, +ssl_ciphers: ?[*:0]const u8 = null, +protos: ?[*:0]const u8 = null, +protos_len: usize = 0, +client_renegotiation_limit: u32 = 0, +client_renegotiation_window: u32 = 0, + +const BlobFileContentResult = struct { + data: [:0]const u8, + + fn init(comptime fieldname: []const u8, js_obj: JSC.JSValue, global: *JSC.JSGlobalObject) bun.JSError!?BlobFileContentResult { + { + const body = try JSC.WebCore.Body.Value.fromJS(global, js_obj); + if (body == .Blob and body.Blob.store != null and body.Blob.store.?.data == .file) { + var fs: JSC.Node.fs.NodeFS = .{}; + const read = fs.readFileWithOptions(.{ .path = body.Blob.store.?.data.file.pathlike }, .sync, .null_terminated); + switch (read) { + .err => { + return global.throwValue(read.err.toJSC(global)); + }, + else => { + const str = read.result.null_terminated; + if (str.len > 0) { + return .{ .data = str }; + } + return global.throwInvalidArguments(std.fmt.comptimePrint("Invalid {s} file", .{fieldname}), .{}); + }, + } + } + } + + return null; + } +}; + +pub fn asUSockets(this: SSLConfig) uws.SocketContext.BunSocketContextOptions { + var ctx_opts: uws.SocketContext.BunSocketContextOptions = .{}; + + if (this.key_file_name != null) + ctx_opts.key_file_name = this.key_file_name; + if (this.cert_file_name != null) + ctx_opts.cert_file_name = this.cert_file_name; + if (this.ca_file_name != null) + ctx_opts.ca_file_name = this.ca_file_name; + if (this.dh_params_file_name != null) + ctx_opts.dh_params_file_name = this.dh_params_file_name; + if (this.passphrase != null) + ctx_opts.passphrase = this.passphrase; + ctx_opts.ssl_prefer_low_memory_usage = @intFromBool(this.low_memory_mode); + + if (this.key) |key| { + ctx_opts.key = key.ptr; + ctx_opts.key_count = this.key_count; + } + if (this.cert) |cert| { + ctx_opts.cert = cert.ptr; + ctx_opts.cert_count = this.cert_count; + } + if (this.ca) |ca| { + ctx_opts.ca = ca.ptr; + ctx_opts.ca_count = this.ca_count; + } + + if (this.ssl_ciphers != null) { + ctx_opts.ssl_ciphers = this.ssl_ciphers; + } + ctx_opts.request_cert = this.request_cert; + ctx_opts.reject_unauthorized = this.reject_unauthorized; + + return ctx_opts; +} + +pub fn isSame(thisConfig: *const SSLConfig, otherConfig: *const SSLConfig) bool { + { //strings + const fields = .{ + "server_name", + "key_file_name", + "cert_file_name", + "ca_file_name", + "dh_params_file_name", + "passphrase", + "ssl_ciphers", + "protos", + }; + + inline for (fields) |field| { + const lhs = @field(thisConfig, field); + const rhs = @field(otherConfig, field); + if (lhs != null and rhs != null) { + if (!stringsEqual(lhs, rhs)) + return false; + } else if (lhs != null or rhs != null) { + return false; + } + } + } + + { + //numbers + const fields = .{ "secure_options", "request_cert", "reject_unauthorized", "low_memory_mode" }; + + inline for (fields) |field| { + const lhs = @field(thisConfig, field); + const rhs = @field(otherConfig, field); + if (lhs != rhs) + return false; + } + } + + { + // complex fields + const fields = .{ "key", "ca", "cert" }; + inline for (fields) |field| { + const lhs_count = @field(thisConfig, field ++ "_count"); + const rhs_count = @field(otherConfig, field ++ "_count"); + if (lhs_count != rhs_count) + return false; + if (lhs_count > 0) { + const lhs = @field(thisConfig, field); + const rhs = @field(otherConfig, field); + for (0..lhs_count) |i| { + if (!stringsEqual(lhs.?[i], rhs.?[i])) + return false; + } + } + } + } + + return true; +} + +fn stringsEqual(a: [*c]const u8, b: [*c]const u8) bool { + const lhs = bun.asByteSlice(a); + const rhs = bun.asByteSlice(b); + return strings.eqlLong(lhs, rhs, true); +} + +pub fn deinit(this: *SSLConfig) void { + const fields = .{ + "server_name", + "key_file_name", + "cert_file_name", + "ca_file_name", + "dh_params_file_name", + "passphrase", + "ssl_ciphers", + "protos", + }; + + inline for (fields) |field| { + if (@field(this, field)) |slice_ptr| { + const slice = std.mem.span(slice_ptr); + if (slice.len > 0) { + bun.freeSensitive(bun.default_allocator, slice); + } + @field(this, field) = ""; + } + } + + if (this.cert) |cert| { + for (0..this.cert_count) |i| { + const slice = std.mem.span(cert[i]); + if (slice.len > 0) { + bun.freeSensitive(bun.default_allocator, slice); + } + } + + bun.default_allocator.free(cert); + this.cert = null; + } + + if (this.key) |key| { + for (0..this.key_count) |i| { + const slice = std.mem.span(key[i]); + if (slice.len > 0) { + bun.freeSensitive(bun.default_allocator, slice); + } + } + + bun.default_allocator.free(key); + this.key = null; + } + + if (this.ca) |ca| { + for (0..this.ca_count) |i| { + const slice = std.mem.span(ca[i]); + if (slice.len > 0) { + bun.freeSensitive(bun.default_allocator, slice); + } + } + + bun.default_allocator.free(ca); + this.ca = null; + } +} + +pub const zero = SSLConfig{}; + +pub fn fromJS(vm: *JSC.VirtualMachine, global: *JSC.JSGlobalObject, obj: JSC.JSValue) bun.JSError!?SSLConfig { + var result = zero; + errdefer result.deinit(); + + var arena: bun.ArenaAllocator = bun.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + + if (!obj.isObject()) { + return global.throwInvalidArguments("tls option expects an object", .{}); + } + + var any = false; + + result.reject_unauthorized = @intFromBool(vm.getTLSRejectUnauthorized()); + + // Required + if (try obj.getTruthy(global, "keyFile")) |key_file_name| { + var sliced = try key_file_name.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.key_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); + if (std.posix.system.access(result.key_file_name, std.posix.F_OK) != 0) { + return global.throwInvalidArguments("Unable to access keyFile path", .{}); + } + any = true; + result.requires_custom_request_ctx = true; + } + } + + if (try obj.getTruthy(global, "key")) |js_obj| { + if (js_obj.jsType().isArray()) { + const count = js_obj.getLength(global); + if (count > 0) { + const native_array = try bun.default_allocator.alloc([*c]const u8, count); + + var valid_count: u32 = 0; + for (0..count) |i| { + const item = js_obj.getIndex(global, @intCast(i)); + if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { + defer sb.deinit(); + const sliced = sb.slice(); + if (sliced.len > 0) { + native_array[valid_count] = try bun.default_allocator.dupeZ(u8, sliced); + valid_count += 1; + any = true; + result.requires_custom_request_ctx = true; + } + } else if (try BlobFileContentResult.init("key", item, global)) |content| { + if (content.data.len > 0) { + native_array[valid_count] = content.data.ptr; + valid_count += 1; + result.requires_custom_request_ctx = true; + any = true; + } else { + // mark and free all CA's + result.cert = native_array; + result.deinit(); + return null; + } + } else { + // mark and free all keys + result.key = native_array; + return global.throwInvalidArguments("key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); + } + } + + if (valid_count == 0) { + bun.default_allocator.free(native_array); + } else { + result.key = native_array; + } + + result.key_count = valid_count; + } + } else if (try BlobFileContentResult.init("key", js_obj, global)) |content| { + if (content.data.len > 0) { + const native_array = try bun.default_allocator.alloc([*c]const u8, 1); + native_array[0] = content.data.ptr; + result.key = native_array; + result.key_count = 1; + any = true; + result.requires_custom_request_ctx = true; + } else { + result.deinit(); + return null; + } + } else { + const native_array = try bun.default_allocator.alloc([*c]const u8, 1); + if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { + defer sb.deinit(); + const sliced = sb.slice(); + if (sliced.len > 0) { + native_array[0] = try bun.default_allocator.dupeZ(u8, sliced); + any = true; + result.requires_custom_request_ctx = true; + result.key = native_array; + result.key_count = 1; + } else { + bun.default_allocator.free(native_array); + } + } else { + // mark and free all certs + result.key = native_array; + return global.throwInvalidArguments("key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); + } + } + } + + if (try obj.getTruthy(global, "certFile")) |cert_file_name| { + var sliced = try cert_file_name.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.cert_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); + if (std.posix.system.access(result.cert_file_name, std.posix.F_OK) != 0) { + return global.throwInvalidArguments("Unable to access certFile path", .{}); + } + any = true; + result.requires_custom_request_ctx = true; + } + } + + if (try obj.getTruthy(global, "ALPNProtocols")) |protocols| { + if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), protocols)) |sb| { + defer sb.deinit(); + const sliced = sb.slice(); + if (sliced.len > 0) { + result.protos = try bun.default_allocator.dupeZ(u8, sliced); + result.protos_len = sliced.len; + } + + any = true; + result.requires_custom_request_ctx = true; + } else { + return global.throwInvalidArguments("ALPNProtocols argument must be an string, Buffer or TypedArray", .{}); + } + } + + if (try obj.getTruthy(global, "cert")) |js_obj| { + if (js_obj.jsType().isArray()) { + const count = js_obj.getLength(global); + if (count > 0) { + const native_array = try bun.default_allocator.alloc([*c]const u8, count); + + var valid_count: u32 = 0; + for (0..count) |i| { + const item = js_obj.getIndex(global, @intCast(i)); + if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { + defer sb.deinit(); + const sliced = sb.slice(); + if (sliced.len > 0) { + native_array[valid_count] = try bun.default_allocator.dupeZ(u8, sliced); + valid_count += 1; + any = true; + result.requires_custom_request_ctx = true; + } + } else if (try BlobFileContentResult.init("cert", item, global)) |content| { + if (content.data.len > 0) { + native_array[valid_count] = content.data.ptr; + valid_count += 1; + result.requires_custom_request_ctx = true; + any = true; + } else { + // mark and free all CA's + result.cert = native_array; + result.deinit(); + return null; + } + } else { + // mark and free all certs + result.cert = native_array; + return global.throwInvalidArguments("cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); + } + } + + if (valid_count == 0) { + bun.default_allocator.free(native_array); + } else { + result.cert = native_array; + } + + result.cert_count = valid_count; + } + } else if (try BlobFileContentResult.init("cert", js_obj, global)) |content| { + if (content.data.len > 0) { + const native_array = try bun.default_allocator.alloc([*c]const u8, 1); + native_array[0] = content.data.ptr; + result.cert = native_array; + result.cert_count = 1; + any = true; + result.requires_custom_request_ctx = true; + } else { + result.deinit(); + return null; + } + } else { + const native_array = try bun.default_allocator.alloc([*c]const u8, 1); + if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { + defer sb.deinit(); + const sliced = sb.slice(); + if (sliced.len > 0) { + native_array[0] = try bun.default_allocator.dupeZ(u8, sliced); + any = true; + result.requires_custom_request_ctx = true; + result.cert = native_array; + result.cert_count = 1; + } else { + bun.default_allocator.free(native_array); + } + } else { + // mark and free all certs + result.cert = native_array; + return global.throwInvalidArguments("cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); + } + } + } + + if (try obj.getBooleanStrict(global, "requestCert")) |request_cert| { + result.request_cert = if (request_cert) 1 else 0; + any = true; + } + + if (try obj.getBooleanStrict(global, "rejectUnauthorized")) |reject_unauthorized| { + result.reject_unauthorized = if (reject_unauthorized) 1 else 0; + any = true; + } + + if (try obj.getTruthy(global, "ciphers")) |ssl_ciphers| { + var sliced = try ssl_ciphers.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.ssl_ciphers = try bun.default_allocator.dupeZ(u8, sliced.slice()); + any = true; + result.requires_custom_request_ctx = true; + } + } + + if (try obj.getTruthy(global, "serverName") orelse try obj.getTruthy(global, "servername")) |server_name| { + var sliced = try server_name.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.server_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); + any = true; + result.requires_custom_request_ctx = true; + } + } + + if (try obj.getTruthy(global, "ca")) |js_obj| { + if (js_obj.jsType().isArray()) { + const count = js_obj.getLength(global); + if (count > 0) { + const native_array = try bun.default_allocator.alloc([*c]const u8, count); + + var valid_count: u32 = 0; + for (0..count) |i| { + const item = js_obj.getIndex(global, @intCast(i)); + if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { + defer sb.deinit(); + const sliced = sb.slice(); + if (sliced.len > 0) { + native_array[valid_count] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable; + valid_count += 1; + any = true; + result.requires_custom_request_ctx = true; + } + } else if (try BlobFileContentResult.init("ca", item, global)) |content| { + if (content.data.len > 0) { + native_array[valid_count] = content.data.ptr; + valid_count += 1; + any = true; + result.requires_custom_request_ctx = true; + } else { + // mark and free all CA's + result.cert = native_array; + result.deinit(); + return null; + } + } else { + // mark and free all CA's + result.cert = native_array; + return global.throwInvalidArguments("ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); + } + } + + if (valid_count == 0) { + bun.default_allocator.free(native_array); + } else { + result.ca = native_array; + } + + result.ca_count = valid_count; + } + } else if (try BlobFileContentResult.init("ca", js_obj, global)) |content| { + if (content.data.len > 0) { + const native_array = try bun.default_allocator.alloc([*c]const u8, 1); + native_array[0] = content.data.ptr; + result.ca = native_array; + result.ca_count = 1; + any = true; + result.requires_custom_request_ctx = true; + } else { + result.deinit(); + return null; + } + } else { + const native_array = try bun.default_allocator.alloc([*c]const u8, 1); + if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { + defer sb.deinit(); + const sliced = sb.slice(); + if (sliced.len > 0) { + native_array[0] = try bun.default_allocator.dupeZ(u8, sliced); + any = true; + result.requires_custom_request_ctx = true; + result.ca = native_array; + result.ca_count = 1; + } else { + bun.default_allocator.free(native_array); + } + } else { + // mark and free all certs + result.ca = native_array; + return global.throwInvalidArguments("ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); + } + } + } + + if (try obj.getTruthy(global, "caFile")) |ca_file_name| { + var sliced = try ca_file_name.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.ca_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); + if (std.posix.system.access(result.ca_file_name, std.posix.F_OK) != 0) { + return global.throwInvalidArguments("Invalid caFile path", .{}); + } + } + } + // Optional + if (any) { + if (try obj.getTruthy(global, "secureOptions")) |secure_options| { + if (secure_options.isNumber()) { + result.secure_options = secure_options.toU32(); + } + } + + if (try obj.getTruthy(global, "clientRenegotiationLimit")) |client_renegotiation_limit| { + if (client_renegotiation_limit.isNumber()) { + result.client_renegotiation_limit = client_renegotiation_limit.toU32(); + } + } + + if (try obj.getTruthy(global, "clientRenegotiationWindow")) |client_renegotiation_window| { + if (client_renegotiation_window.isNumber()) { + result.client_renegotiation_window = client_renegotiation_window.toU32(); + } + } + + if (try obj.getTruthy(global, "dhParamsFile")) |dh_params_file_name| { + var sliced = try dh_params_file_name.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.dh_params_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); + if (std.posix.system.access(result.dh_params_file_name, std.posix.F_OK) != 0) { + return global.throwInvalidArguments("Invalid dhParamsFile path", .{}); + } + } + } + + if (try obj.getTruthy(global, "passphrase")) |passphrase| { + var sliced = try passphrase.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.passphrase = try bun.default_allocator.dupeZ(u8, sliced.slice()); + } + } + + if (try obj.get(global, "lowMemoryMode")) |low_memory_mode| { + if (low_memory_mode.isBoolean() or low_memory_mode.isUndefined()) { + result.low_memory_mode = low_memory_mode.toBoolean(); + any = true; + } else { + return global.throw("Expected lowMemoryMode to be a boolean", .{}); + } + } + } + + if (!any) + return null; + return result; +} + +const std = @import("std"); +const bun = @import("bun"); +const JSC = bun.JSC; +const uws = bun.uws; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const VirtualMachine = JSC.VirtualMachine; +const strings = bun.strings; diff --git a/src/bun.js/api/server/ServerConfig.zig b/src/bun.js/api/server/ServerConfig.zig new file mode 100644 index 0000000000..88a3effc7b --- /dev/null +++ b/src/bun.js/api/server/ServerConfig.zig @@ -0,0 +1,1092 @@ +const ServerConfig = @This(); + +address: union(enum) { + tcp: struct { + port: u16 = 0, + hostname: ?[*:0]const u8 = null, + }, + unix: [:0]const u8, + + pub fn deinit(this: *@This(), allocator: std.mem.Allocator) void { + switch (this.*) { + .tcp => |tcp| { + if (tcp.hostname) |host| { + allocator.free(bun.sliceTo(host, 0)); + } + }, + .unix => |addr| { + allocator.free(addr); + }, + } + this.* = .{ .tcp = .{} }; + } +} = .{ + .tcp = .{}, +}, +idleTimeout: u8 = 10, //TODO: should we match websocket default idleTimeout of 120? +has_idleTimeout: bool = false, +// TODO: use webkit URL parser instead of bun's +base_url: URL = URL{}, +base_uri: string = "", + +ssl_config: ?SSLConfig = null, +sni: ?bun.BabyList(SSLConfig) = null, +max_request_body_size: usize = 1024 * 1024 * 128, +development: DevelopmentOption = .development, +broadcast_console_log_from_browser_to_server_for_bake: bool = false, + +/// Enable automatic workspace folders for Chrome DevTools +/// https://chromium.googlesource.com/devtools/devtools-frontend/+/main/docs/ecosystem/automatic_workspace_folders.md +/// https://github.com/ChromeDevTools/vite-plugin-devtools-json/blob/76080b04422b36230d4b7a674b90d6df296cbff5/src/index.ts#L60-L77 +/// +/// If HMR is not enabled, then this field is ignored. +enable_chrome_devtools_automatic_workspace_folders: bool = true, + +onError: JSC.JSValue = JSC.JSValue.zero, +onRequest: JSC.JSValue = JSC.JSValue.zero, +onNodeHTTPRequest: JSC.JSValue = JSC.JSValue.zero, + +websocket: ?WebSocketServerContext = null, + +inspector: bool = false, +reuse_port: bool = false, +id: []const u8 = "", +allow_hot: bool = true, +ipv6_only: bool = false, + +is_node_http: bool = false, +had_routes_object: bool = false, + +static_routes: std.ArrayList(StaticRouteEntry) = std.ArrayList(StaticRouteEntry).init(bun.default_allocator), +negative_routes: std.ArrayList([:0]const u8) = std.ArrayList([:0]const u8).init(bun.default_allocator), +user_routes_to_build: std.ArrayList(UserRouteBuilder) = std.ArrayList(UserRouteBuilder).init(bun.default_allocator), + +bake: ?bun.bake.UserOptions = null, + +pub const DevelopmentOption = enum { + development, + production, + development_without_hmr, + + pub fn isHMREnabled(this: DevelopmentOption) bool { + return this == .development; + } + + pub fn isDevelopment(this: DevelopmentOption) bool { + return this == .development or this == .development_without_hmr; + } +}; + +pub fn isDevelopment(this: *const ServerConfig) bool { + return this.development.isDevelopment(); +} + +pub fn memoryCost(this: *const ServerConfig) usize { + // ignore @sizeOf(ServerConfig), assume already included. + var cost: usize = 0; + for (this.static_routes.items) |*entry| { + cost += entry.memoryCost(); + } + cost += this.id.len; + cost += this.base_url.href.len; + for (this.negative_routes.items) |route| { + cost += route.len; + } + + return cost; +} + +// We need to be able to apply the route to multiple Apps even when there is only one RouteList. +pub const RouteDeclaration = struct { + path: [:0]const u8 = "", + method: union(enum) { + any: void, + specific: HTTP.Method, + } = .any, + + pub fn deinit(this: *RouteDeclaration) void { + if (this.path.len > 0) { + bun.default_allocator.free(this.path); + } + } +}; + +// TODO: rename to StaticRoute.Entry +pub const StaticRouteEntry = struct { + path: []const u8, + route: AnyRoute, + method: HTTP.Method.Optional = .any, + + pub fn memoryCost(this: *const StaticRouteEntry) usize { + return this.path.len + this.route.memoryCost(); + } + + /// Clone the path buffer and increment the ref count + /// This doesn't actually clone the route, it just increments the ref count + pub fn clone(this: StaticRouteEntry) !StaticRouteEntry { + this.route.ref(); + + return .{ + .path = try bun.default_allocator.dupe(u8, this.path), + .route = this.route, + .method = this.method, + }; + } + + pub fn deinit(this: *StaticRouteEntry) void { + bun.default_allocator.free(this.path); + this.path = ""; + this.route.deref(); + this.* = undefined; + } + + pub fn isLessThan(_: void, this: StaticRouteEntry, other: StaticRouteEntry) bool { + return strings.cmpStringsDesc({}, this.path, other.path); + } +}; + +fn normalizeStaticRoutesList(this: *ServerConfig) !void { + const Context = struct { + // Ac + pub fn hash(route: *StaticRouteEntry) u64 { + var hasher = std.hash.Wyhash.init(0); + switch (route.method) { + .any => hasher.update("ANY"), + .method => |*set| { + var iter = set.iterator(); + while (iter.next()) |method| { + hasher.update(@tagName(method)); + } + }, + } + hasher.update(route.path); + return hasher.final(); + } + }; + + var static_routes_dedupe_list = std.ArrayList(u64).init(bun.default_allocator); + try static_routes_dedupe_list.ensureTotalCapacity(@truncate(this.static_routes.items.len)); + defer static_routes_dedupe_list.deinit(); + + // Iterate through the list of static routes backwards + // Later ones added override earlier ones + var list = &this.static_routes; + if (list.items.len > 0) { + var index = list.items.len - 1; + while (true) { + const route = &list.items[index]; + const hash = Context.hash(route); + if (std.mem.indexOfScalar(u64, static_routes_dedupe_list.items, hash) != null) { + var item = list.orderedRemove(index); + item.deinit(); + } else { + try static_routes_dedupe_list.append(hash); + } + + if (index == 0) break; + index -= 1; + } + } + + // sort the cloned static routes by name for determinism + std.mem.sort(StaticRouteEntry, list.items, {}, StaticRouteEntry.isLessThan); +} + +pub fn cloneForReloadingStaticRoutes(this: *ServerConfig) !ServerConfig { + var that = this.*; + this.ssl_config = null; + this.sni = null; + this.address = .{ .tcp = .{} }; + this.websocket = null; + this.bake = null; + + try that.normalizeStaticRoutesList(); + + return that; +} + +pub fn appendStaticRoute(this: *ServerConfig, path: []const u8, route: AnyRoute, method: HTTP.Method.Optional) !void { + try this.static_routes.append(StaticRouteEntry{ + .path = try bun.default_allocator.dupe(u8, path), + .route = route, + .method = method, + }); +} + +pub fn applyStaticRoute(server: AnyServer, comptime ssl: bool, app: *uws.NewApp(ssl), comptime T: type, entry: T, path: []const u8, method: HTTP.Method.Optional) void { + entry.server = server; + const handler_wrap = struct { + pub fn handler(route: T, req: *uws.Request, resp: *uws.NewApp(ssl).Response) void { + route.onRequest(req, switch (comptime ssl) { + true => .{ .SSL = resp }, + false => .{ .TCP = resp }, + }); + } + + pub fn HEAD(route: T, req: *uws.Request, resp: *uws.NewApp(ssl).Response) void { + route.onHEADRequest(req, switch (comptime ssl) { + true => .{ .SSL = resp }, + false => .{ .TCP = resp }, + }); + } + }; + app.head(path, T, entry, handler_wrap.HEAD); + switch (method) { + .any => { + app.any(path, T, entry, handler_wrap.handler); + }, + .method => |*m| { + var iter = m.iterator(); + while (iter.next()) |method_| { + app.method(method_, path, T, entry, handler_wrap.handler); + } + }, + } +} + +pub fn deinit(this: *ServerConfig) void { + this.address.deinit(bun.default_allocator); + + for (this.negative_routes.items) |route| { + bun.default_allocator.free(route); + } + this.negative_routes.clearAndFree(); + + if (this.base_url.href.len > 0) { + bun.default_allocator.free(this.base_url.href); + this.base_url = URL{}; + } + if (this.ssl_config) |*ssl_config| { + ssl_config.deinit(); + this.ssl_config = null; + } + if (this.sni) |sni| { + for (sni.slice()) |*ssl_config| { + ssl_config.deinit(); + } + this.sni.?.deinitWithAllocator(bun.default_allocator); + this.sni = null; + } + + for (this.static_routes.items) |*entry| { + entry.deinit(); + } + this.static_routes.clearAndFree(); + + if (this.bake) |*bake| { + bake.deinit(); + } + + for (this.user_routes_to_build.items) |*builder| { + builder.deinit(); + } + this.user_routes_to_build.clearAndFree(); +} + +pub fn computeID(this: *const ServerConfig, allocator: std.mem.Allocator) []const u8 { + var arraylist = std.ArrayList(u8).init(allocator); + var writer = arraylist.writer(); + + writer.writeAll("[http]-") catch {}; + switch (this.address) { + .tcp => { + if (this.address.tcp.hostname) |host| { + writer.print("tcp:{s}:{d}", .{ + bun.sliceTo(host, 0), + this.address.tcp.port, + }) catch {}; + } else { + writer.print("tcp:localhost:{d}", .{ + this.address.tcp.port, + }) catch {}; + } + }, + .unix => { + writer.print("unix:{s}", .{ + bun.sliceTo(this.address.unix, 0), + }) catch {}; + }, + } + + return arraylist.items; +} + +pub fn getUsocketsOptions(this: *const ServerConfig) i32 { + // Unlike Node.js, we set exclusive port in case reuse port is not set + var out: i32 = if (this.reuse_port) + uws.LIBUS_LISTEN_REUSE_PORT | uws.LIBUS_LISTEN_REUSE_ADDR + else + uws.LIBUS_LISTEN_EXCLUSIVE_PORT; + + if (this.ipv6_only) { + out |= uws.LIBUS_SOCKET_IPV6_ONLY; + } + + return out; +} + +fn validateRouteName(global: *JSC.JSGlobalObject, path: []const u8) !void { + // Already validated by the caller + bun.debugAssert(path.len > 0 and path[0] == '/'); + + // For now, we don't support params that start with a number. + // Mostly because it makes the params object more complicated to implement and it's easier to cut scope this way for now. + var remaining = path; + var duped_route_names = bun.StringHashMap(void).init(bun.default_allocator); + defer duped_route_names.deinit(); + while (strings.indexOfChar(remaining, ':')) |index| { + remaining = remaining[index + 1 ..]; + const end = strings.indexOfChar(remaining, '/') orelse remaining.len; + const route_name = remaining[0..end]; + if (route_name.len > 0 and std.ascii.isDigit(route_name[0])) { + return global.throwTODO( + \\Route parameter names cannot start with a number. + \\ + \\If you run into this, please file an issue and we will add support for it. + ); + } + + const entry = duped_route_names.getOrPut(route_name) catch bun.outOfMemory(); + if (entry.found_existing) { + return global.throwTODO( + \\Support for duplicate route parameter names is not yet implemented. + \\ + \\If you run into this, please file an issue and we will add support for it. + ); + } + + remaining = remaining[end..]; + } +} + +pub const SSLConfig = @import("./SSLConfig.zig"); + +fn getRoutesObject(global: *JSC.JSGlobalObject, arg: JSC.JSValue) bun.JSError!?JSC.JSValue { + inline for (.{ "routes", "static" }) |key| { + if (try arg.get(global, key)) |routes| { + // https://github.com/oven-sh/bun/issues/17568 + if (routes.isArray()) { + return null; + } + return routes; + } + } + return null; +} + +pub const FromJSOptions = struct { + allow_bake_config: bool = true, + is_fetch_required: bool = true, + has_user_routes: bool = false, +}; + +pub fn fromJS( + global: *JSC.JSGlobalObject, + args: *ServerConfig, + arguments: *JSC.CallFrame.ArgumentsSlice, + opts: FromJSOptions, +) bun.JSError!void { + const vm = arguments.vm; + const env = vm.transpiler.env; + + args.* = .{ + .address = .{ + .tcp = .{ + .port = 3000, + .hostname = null, + }, + }, + .development = if (vm.transpiler.options.transform_options.serve_hmr) |hmr| + if (!hmr) .development_without_hmr else .development + else + .development, + + // If this is a node:cluster child, let's default to SO_REUSEPORT. + // That way you don't have to remember to set reusePort: true in Bun.serve() when using node:cluster. + .reuse_port = env.get("NODE_UNIQUE_ID") != null, + }; + var has_hostname = false; + + defer { + if (!args.development.isHMREnabled()) { + bun.assert(args.bake == null); + } + } + + if (strings.eqlComptime(env.get("NODE_ENV") orelse "", "production")) { + args.development = .production; + } + + if (arguments.vm.transpiler.options.production) { + args.development = .production; + } + + args.address.tcp.port = brk: { + const PORT_ENV = .{ "BUN_PORT", "PORT", "NODE_PORT" }; + + inline for (PORT_ENV) |PORT| { + if (env.get(PORT)) |port| { + if (std.fmt.parseInt(u16, port, 10)) |_port| { + break :brk _port; + } else |_| {} + } + } + + if (arguments.vm.transpiler.options.transform_options.port) |port| { + break :brk port; + } + + break :brk args.address.tcp.port; + }; + var port = args.address.tcp.port; + + if (arguments.vm.transpiler.options.transform_options.origin) |origin| { + args.base_uri = try bun.default_allocator.dupeZ(u8, origin); + } + + defer { + if (global.hasException()) { + if (args.ssl_config) |*conf| { + conf.deinit(); + args.ssl_config = null; + } + } + } + + if (arguments.next()) |arg| { + if (!arg.isObject()) { + return global.throwInvalidArguments("Bun.serve expects an object", .{}); + } + + // "development" impacts other settings like bake. + if (try arg.get(global, "development")) |dev| { + if (dev.isObject()) { + if (try dev.getBooleanStrict(global, "hmr")) |hmr| { + args.development = if (!hmr) .development_without_hmr else .development; + } else { + args.development = .development; + } + + if (try dev.getBooleanStrict(global, "console")) |console| { + args.broadcast_console_log_from_browser_to_server_for_bake = console; + } + + if (try dev.getBooleanStrict(global, "chromeDevToolsAutomaticWorkspaceFolders")) |enable_chrome_devtools_automatic_workspace_folders| { + args.enable_chrome_devtools_automatic_workspace_folders = enable_chrome_devtools_automatic_workspace_folders; + } + } else { + args.development = if (dev.toBoolean()) .development else .production; + } + args.reuse_port = args.development == .production; + } + if (global.hasException()) return error.JSError; + + if (try getRoutesObject(global, arg)) |static| { + const static_obj = static.getObject() orelse { + return global.throwInvalidArguments( + \\Bun.serve() expects 'routes' to be an object shaped like: + \\ + \\ { + \\ "/path": { + \\ GET: (req) => new Response("Hello"), + \\ POST: (req) => new Response("Hello"), + \\ }, + \\ "/path2/:param": new Response("Hello"), + \\ "/path3/:param1/:param2": (req) => new Response("Hello") + \\ } + \\ + \\Learn more at https://bun.sh/docs/api/http + , .{}); + }; + args.had_routes_object = true; + + var iter = try JSC.JSPropertyIterator(.{ + .skip_empty_name = true, + .include_value = true, + }).init(global, static_obj); + defer iter.deinit(); + + var init_ctx: AnyRoute.ServerInitContext = .{ + .arena = .init(bun.default_allocator), + .dedupe_html_bundle_map = .init(bun.default_allocator), + .framework_router_list = .init(bun.default_allocator), + .js_string_allocations = .empty, + }; + errdefer { + init_ctx.arena.deinit(); + init_ctx.framework_router_list.deinit(); + } + // This list is not used in the success case + defer init_ctx.dedupe_html_bundle_map.deinit(); + + var framework_router_list = std.ArrayList(bun.bake.FrameworkRouter.Type).init(bun.default_allocator); + errdefer framework_router_list.deinit(); + + errdefer { + for (args.static_routes.items) |*static_route| { + static_route.deinit(); + } + args.static_routes.clearAndFree(); + } + + while (try iter.next()) |key| { + const path, const is_ascii = key.toOwnedSliceReturningAllASCII(bun.default_allocator) catch bun.outOfMemory(); + errdefer bun.default_allocator.free(path); + + const value: JSC.JSValue = iter.value; + + if (value.isUndefined()) { + continue; + } + + if (path.len == 0 or (path[0] != '/')) { + return global.throwInvalidArguments("Invalid route {}. Path must start with '/'", .{bun.fmt.quote(path)}); + } + + if (!is_ascii) { + return global.throwInvalidArguments("Invalid route {}. Please encode all non-ASCII characters in the path.", .{bun.fmt.quote(path)}); + } + + if (value == .false) { + const duped = bun.default_allocator.dupeZ(u8, path) catch bun.outOfMemory(); + defer bun.default_allocator.free(path); + args.negative_routes.append(duped) catch bun.outOfMemory(); + continue; + } + + if (value.isCallable()) { + try validateRouteName(global, path); + args.user_routes_to_build.append(.{ + .route = .{ + .path = bun.default_allocator.dupeZ(u8, path) catch bun.outOfMemory(), + .method = .any, + }, + .callback = .create(value.withAsyncContextIfNeeded(global), global), + }) catch bun.outOfMemory(); + bun.default_allocator.free(path); + continue; + } else if (value.isObject()) { + const methods = .{ + HTTP.Method.CONNECT, + HTTP.Method.DELETE, + HTTP.Method.GET, + HTTP.Method.HEAD, + HTTP.Method.OPTIONS, + HTTP.Method.PATCH, + HTTP.Method.POST, + HTTP.Method.PUT, + HTTP.Method.TRACE, + }; + var found = false; + inline for (methods) |method| { + if (value.getOwn(global, @tagName(method))) |function| { + if (!found) { + try validateRouteName(global, path); + } + found = true; + + if (function.isCallable()) { + args.user_routes_to_build.append(.{ + .route = .{ + .path = bun.default_allocator.dupeZ(u8, path) catch bun.outOfMemory(), + .method = .{ .specific = method }, + }, + .callback = .create(function.withAsyncContextIfNeeded(global), global), + }) catch bun.outOfMemory(); + } else if (try AnyRoute.fromJS(global, path, function, &init_ctx)) |html_route| { + var method_set = bun.http.Method.Set.initEmpty(); + method_set.insert(method); + + args.static_routes.append(.{ + .path = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(), + .route = html_route, + .method = .{ .method = method_set }, + }) catch bun.outOfMemory(); + } + } + } + + if (found) { + bun.default_allocator.free(path); + continue; + } + } + + const route = try AnyRoute.fromJS(global, path, value, &init_ctx) orelse { + return global.throwInvalidArguments( + \\'routes' expects a Record Response|Promise}> + \\ + \\To bundle frontend apps on-demand with Bun.serve(), import HTML files. + \\ + \\Example: + \\ + \\```js + \\import { serve } from "bun"; + \\import app from "./app.html"; + \\ + \\serve({ + \\ routes: { + \\ "/index.json": Response.json({ message: "Hello World" }), + \\ "/app": app, + \\ "/path/:param": (req) => { + \\ const param = req.params.param; + \\ return Response.json({ message: `Hello ${param}` }); + \\ }, + \\ "/path": { + \\ GET(req) { + \\ return Response.json({ message: "Hello World" }); + \\ }, + \\ POST(req) { + \\ return Response.json({ message: "Hello World" }); + \\ }, + \\ }, + \\ }, + \\ + \\ fetch(request) { + \\ return new Response("fallback response"); + \\ }, + \\}); + \\``` + \\ + \\See https://bun.sh/docs/api/http for more information. + , + .{}, + ); + }; + args.static_routes.append(.{ + .path = path, + .route = route, + }) catch bun.outOfMemory(); + } + + // When HTML bundles are provided, ensure DevServer options are ready + // The presence of these options causes Bun.serve to initialize things. + if ((init_ctx.dedupe_html_bundle_map.count() > 0 or + init_ctx.framework_router_list.items.len > 0)) + { + if (args.development.isHMREnabled()) { + const root = bun.fs.FileSystem.instance.top_level_dir; + const framework = try bun.bake.Framework.auto( + init_ctx.arena.allocator(), + &global.bunVM().transpiler.resolver, + init_ctx.framework_router_list.items, + ); + args.bake = .{ + .arena = init_ctx.arena, + .allocations = init_ctx.js_string_allocations, + .root = root, + .framework = framework, + .bundler_options = bun.bake.SplitBundlerOptions.empty, + }; + const bake = &args.bake.?; + + const o = vm.transpiler.options.transform_options; + + switch (o.serve_env_behavior) { + .prefix => { + bake.bundler_options.client.env_prefix = vm.transpiler.options.transform_options.serve_env_prefix; + bake.bundler_options.client.env = .prefix; + }, + .load_all => { + bake.bundler_options.client.env = .load_all; + }, + .disable => { + bake.bundler_options.client.env = .disable; + }, + else => {}, + } + + if (o.serve_define) |define| { + bake.bundler_options.client.define = define; + bake.bundler_options.server.define = define; + bake.bundler_options.ssr.define = define; + } + } else { + if (init_ctx.framework_router_list.items.len > 0) { + return global.throwInvalidArguments("FrameworkRouter is currently only supported when `development: true`", .{}); + } + init_ctx.arena.deinit(); + } + } else { + bun.debugAssert(init_ctx.arena.state.end_index == 0 and + init_ctx.arena.state.buffer_list.first == null); + init_ctx.arena.deinit(); + } + } + + if (global.hasException()) return error.JSError; + + if (try arg.get(global, "idleTimeout")) |value| { + if (!value.isUndefinedOrNull()) { + if (!value.isAnyInt()) { + return global.throwInvalidArguments("Bun.serve expects idleTimeout to be an integer", .{}); + } + args.has_idleTimeout = true; + + const idleTimeout: u64 = @intCast(@max(value.toInt64(), 0)); + if (idleTimeout > 255) { + return global.throwInvalidArguments("Bun.serve expects idleTimeout to be 255 or less", .{}); + } + + args.idleTimeout = @truncate(idleTimeout); + } + } + + if (try arg.getTruthy(global, "webSocket") orelse try arg.getTruthy(global, "websocket")) |websocket_object| { + if (!websocket_object.isObject()) { + if (args.ssl_config) |*conf| { + conf.deinit(); + } + return global.throwInvalidArguments("Expected websocket to be an object", .{}); + } + + errdefer if (args.ssl_config) |*conf| conf.deinit(); + args.websocket = try WebSocketServerContext.onCreate(global, websocket_object); + } + if (global.hasException()) return error.JSError; + + if (try arg.getTruthy(global, "port")) |port_| { + args.address.tcp.port = @as( + u16, + @intCast(@min( + @max(0, port_.coerce(i32, global)), + std.math.maxInt(u16), + )), + ); + port = args.address.tcp.port; + } + if (global.hasException()) return error.JSError; + + if (try arg.getTruthy(global, "baseURI")) |baseURI| { + var sliced = try baseURI.toSlice(global, bun.default_allocator); + + if (sliced.len > 0) { + defer sliced.deinit(); + if (args.base_uri.len > 0) { + bun.default_allocator.free(@constCast(args.base_uri)); + } + args.base_uri = bun.default_allocator.dupe(u8, sliced.slice()) catch unreachable; + } + } + if (global.hasException()) return error.JSError; + + 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) { + args.address.tcp.hostname = bun.default_allocator.dupeZ(u8, host_str.slice()) catch unreachable; + has_hostname = true; + } + } + if (global.hasException()) return error.JSError; + + 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) { + return global.throwInvalidArguments("Cannot specify both hostname and unix", .{}); + } + + args.address = .{ .unix = bun.default_allocator.dupeZ(u8, unix_str.slice()) catch unreachable }; + } + } + if (global.hasException()) return error.JSError; + + if (try arg.get(global, "id")) |id| { + if (id.isUndefinedOrNull()) { + args.allow_hot = false; + } else { + const id_str = try id.toSlice( + global, + bun.default_allocator, + ); + + if (id_str.len > 0) { + args.id = (id_str.cloneIfNeeded(bun.default_allocator) catch unreachable).slice(); + } else { + args.allow_hot = false; + } + } + } + if (global.hasException()) return error.JSError; + + if (opts.allow_bake_config) { + if (try arg.getTruthy(global, "app")) |bake_args_js| brk: { + if (!bun.FeatureFlags.bake()) { + break :brk; + } + if (args.bake != null) { + // "app" is likely to be removed in favor of the HTML loader. + return global.throwInvalidArguments("'app' + HTML loader not supported.", .{}); + } + + if (args.development == .production) { + return global.throwInvalidArguments("TODO: 'development: false' in serve options with 'app'. For now, use `bun build --app` or set 'development: true'", .{}); + } + + args.bake = try bun.bake.UserOptions.fromJS(bake_args_js, global); + } + } + + if (try arg.get(global, "reusePort")) |dev| { + args.reuse_port = dev.coerce(bool, global); + } + if (global.hasException()) return error.JSError; + + if (try arg.get(global, "ipv6Only")) |dev| { + args.ipv6_only = dev.coerce(bool, global); + } + if (global.hasException()) return error.JSError; + + if (try arg.get(global, "inspector")) |inspector| { + args.inspector = inspector.coerce(bool, global); + + if (args.inspector and args.development == .production) { + return global.throwInvalidArguments("Cannot enable inspector in production. Please set development: true in Bun.serve()", .{}); + } + } + if (global.hasException()) return error.JSError; + + if (try arg.getTruthy(global, "maxRequestBodySize")) |max_request_body_size| { + if (max_request_body_size.isNumber()) { + args.max_request_body_size = @as(u64, @intCast(@max(0, max_request_body_size.toInt64()))); + } + } + if (global.hasException()) return error.JSError; + + if (try arg.getTruthyComptime(global, "error")) |onError| { + if (!onError.isCallable()) { + return global.throwInvalidArguments("Expected error to be a function", .{}); + } + const onErrorSnapshot = onError.withAsyncContextIfNeeded(global); + args.onError = onErrorSnapshot; + onErrorSnapshot.protect(); + } + if (global.hasException()) return error.JSError; + + if (try arg.getTruthy(global, "onNodeHTTPRequest")) |onRequest_| { + if (!onRequest_.isCallable()) { + return global.throwInvalidArguments("Expected onNodeHTTPRequest to be a function", .{}); + } + const onRequest = onRequest_.withAsyncContextIfNeeded(global); + onRequest.protect(); + args.onNodeHTTPRequest = onRequest; + } + + if (try arg.getTruthy(global, "fetch")) |onRequest_| { + if (!onRequest_.isCallable()) { + return global.throwInvalidArguments("Expected fetch() to be a function", .{}); + } + const onRequest = onRequest_.withAsyncContextIfNeeded(global); + onRequest.protect(); + args.onRequest = onRequest; + } else if (args.bake == null and args.onNodeHTTPRequest == .zero and ((args.static_routes.items.len + args.user_routes_to_build.items.len) == 0 and !opts.has_user_routes) and opts.is_fetch_required) { + if (global.hasException()) return error.JSError; + return global.throwInvalidArguments( + \\Bun.serve() needs either: + \\ + \\ - A routes object: + \\ routes: { + \\ "/path": { + \\ GET: (req) => new Response("Hello") + \\ } + \\ } + \\ + \\ - Or a fetch handler: + \\ fetch: (req) => { + \\ return new Response("Hello") + \\ } + \\ + \\Learn more at https://bun.sh/docs/api/http + , .{}); + } else { + if (global.hasException()) return error.JSError; + } + + if (try arg.getTruthy(global, "tls")) |tls| { + if (tls.isFalsey()) { + args.ssl_config = null; + } else if (tls.jsType().isArray()) { + var value_iter = tls.arrayIterator(global); + if (value_iter.len == 1) { + return global.throwInvalidArguments("tls option expects at least 1 tls object", .{}); + } + while (value_iter.next()) |item| { + var ssl_config = try SSLConfig.fromJS(vm, global, item) orelse { + if (global.hasException()) { + return error.JSError; + } + + // Backwards-compatibility; we ignored empty tls objects. + continue; + }; + + if (args.ssl_config == null) { + args.ssl_config = ssl_config; + } else { + if (ssl_config.server_name == null or std.mem.span(ssl_config.server_name).len == 0) { + defer ssl_config.deinit(); + return global.throwInvalidArguments("SNI tls object must have a serverName", .{}); + } + if (args.sni == null) { + args.sni = bun.BabyList(SSLConfig).initCapacity(bun.default_allocator, value_iter.len - 1) catch bun.outOfMemory(); + } + + args.sni.?.push(bun.default_allocator, ssl_config) catch bun.outOfMemory(); + } + } + } else { + if (try SSLConfig.fromJS(vm, global, tls)) |ssl_config| { + args.ssl_config = ssl_config; + } + if (global.hasException()) { + return error.JSError; + } + } + } + if (global.hasException()) return error.JSError; + + // @compatibility Bun v0.x - v0.2.1 + // this used to be top-level, now it's "tls" object + if (args.ssl_config == null) { + if (try SSLConfig.fromJS(vm, global, arg)) |ssl_config| { + args.ssl_config = ssl_config; + } + if (global.hasException()) { + return error.JSError; + } + } + } else { + return global.throwInvalidArguments("Bun.serve expects an object", .{}); + } + + if (args.base_uri.len > 0) { + args.base_url = URL.parse(args.base_uri); + if (args.base_url.hostname.len == 0) { + bun.default_allocator.free(@constCast(args.base_uri)); + args.base_uri = ""; + return global.throwInvalidArguments("baseURI must have a hostname", .{}); + } + + if (!strings.isAllASCII(args.base_uri)) { + bun.default_allocator.free(@constCast(args.base_uri)); + args.base_uri = ""; + return global.throwInvalidArguments("Unicode baseURI must already be encoded for now.\nnew URL(baseuRI).toString() should do the trick.", .{}); + } + + if (args.base_url.protocol.len == 0) { + const protocol: string = if (args.ssl_config != null) "https" else "http"; + const hostname = args.base_url.hostname; + const needsBrackets: bool = strings.isIPV6Address(hostname) and hostname[0] != '['; + const original_base_uri = args.base_uri; + defer bun.default_allocator.free(@constCast(original_base_uri)); + if (needsBrackets) { + args.base_uri = (if ((port == 80 and args.ssl_config == null) or (port == 443 and args.ssl_config != null)) + std.fmt.allocPrint(bun.default_allocator, "{s}://[{s}]/{s}", .{ + protocol, + hostname, + strings.trimLeadingChar(args.base_url.pathname, '/'), + }) + else + std.fmt.allocPrint(bun.default_allocator, "{s}://[{s}]:{d}/{s}", .{ + protocol, + hostname, + port, + strings.trimLeadingChar(args.base_url.pathname, '/'), + })) catch unreachable; + } else { + args.base_uri = (if ((port == 80 and args.ssl_config == null) or (port == 443 and args.ssl_config != null)) + std.fmt.allocPrint(bun.default_allocator, "{s}://{s}/{s}", .{ + protocol, + hostname, + strings.trimLeadingChar(args.base_url.pathname, '/'), + }) + else + std.fmt.allocPrint(bun.default_allocator, "{s}://{s}:{d}/{s}", .{ + protocol, + hostname, + port, + strings.trimLeadingChar(args.base_url.pathname, '/'), + })) catch unreachable; + } + + args.base_url = URL.parse(args.base_uri); + } + } else { + const hostname: string = + if (has_hostname) std.mem.span(args.address.tcp.hostname.?) else "0.0.0.0"; + + const needsBrackets: bool = strings.isIPV6Address(hostname) and hostname[0] != '['; + + const protocol: string = if (args.ssl_config != null) "https" else "http"; + if (needsBrackets) { + args.base_uri = (if ((port == 80 and args.ssl_config == null) or (port == 443 and args.ssl_config != null)) + std.fmt.allocPrint(bun.default_allocator, "{s}://[{s}]/", .{ + protocol, + hostname, + }) + else + std.fmt.allocPrint(bun.default_allocator, "{s}://[{s}]:{d}/", .{ protocol, hostname, port })) catch unreachable; + } else { + args.base_uri = (if ((port == 80 and args.ssl_config == null) or (port == 443 and args.ssl_config != null)) + std.fmt.allocPrint(bun.default_allocator, "{s}://{s}/", .{ + protocol, + hostname, + }) + else + std.fmt.allocPrint(bun.default_allocator, "{s}://{s}:{d}/", .{ protocol, hostname, port })) catch unreachable; + } + + if (!strings.isAllASCII(hostname)) { + bun.default_allocator.free(@constCast(args.base_uri)); + args.base_uri = ""; + return global.throwInvalidArguments("Unicode hostnames must already be encoded for now.\nnew URL(input).hostname should do the trick.", .{}); + } + + args.base_url = URL.parse(args.base_uri); + } + + // I don't think there's a case where this can happen + // but let's check anyway, just in case + if (args.base_url.hostname.len == 0) { + bun.default_allocator.free(@constCast(args.base_uri)); + args.base_uri = ""; + return global.throwInvalidArguments("baseURI must have a hostname", .{}); + } + + if (args.base_url.username.len > 0 or args.base_url.password.len > 0) { + bun.default_allocator.free(@constCast(args.base_uri)); + args.base_uri = ""; + return global.throwInvalidArguments("baseURI can't have a username or password", .{}); + } + + return; +} + +const UserRouteBuilder = struct { + route: ServerConfig.RouteDeclaration, + callback: JSC.Strong.Optional = .empty, + + pub fn deinit(this: *UserRouteBuilder) void { + this.route.deinit(); + this.callback.deinit(); + } +}; + +const std = @import("std"); +const bun = @import("bun"); +const strings = bun.strings; +const URL = bun.URL; +const JSC = bun.JSC; +const JSError = bun.JSError; +const assert = bun.assert; +const string = []const u8; +const WebSocketServerContext = @import("./WebSocketServerContext.zig"); +const AnyRoute = @import("../server.zig").AnyRoute; +const HTTP = bun.http; +const uws = bun.uws; +const AnyServer = JSC.API.AnyServer; diff --git a/src/bun.js/api/server/ServerWebSocket.zig b/src/bun.js/api/server/ServerWebSocket.zig index 1d76a05d41..ef13bb044b 100644 --- a/src/bun.js/api/server/ServerWebSocket.zig +++ b/src/bun.js/api/server/ServerWebSocket.zig @@ -1289,6 +1289,6 @@ const bun = @import("bun"); const string = []const u8; const std = @import("std"); const ZigString = JSC.ZigString; -const WebSocketServer = @import("../server.zig").WebSocketServer; +const WebSocketServer = @import("../server.zig").WebSocketServerContext; const uws = bun.uws; const Output = bun.Output; diff --git a/src/bun.js/api/server/WebSocketServerContext.zig b/src/bun.js/api/server/WebSocketServerContext.zig new file mode 100644 index 0000000000..db0b9196f4 --- /dev/null +++ b/src/bun.js/api/server/WebSocketServerContext.zig @@ -0,0 +1,322 @@ +const WebSocketServerContext = @This(); + +globalObject: *JSC.JSGlobalObject = undefined, +handler: Handler = .{}, + +maxPayloadLength: u32 = 1024 * 1024 * 16, // 16MB +maxLifetime: u16 = 0, +idleTimeout: u16 = 120, // 2 minutes +compression: i32 = 0, +backpressureLimit: u32 = 1024 * 1024 * 16, // 16MB +sendPingsAutomatically: bool = true, +resetIdleTimeoutOnSend: bool = true, +closeOnBackpressureLimit: bool = false, + +pub const Handler = struct { + onOpen: JSC.JSValue = .zero, + onMessage: JSC.JSValue = .zero, + onClose: JSC.JSValue = .zero, + onDrain: JSC.JSValue = .zero, + onError: JSC.JSValue = .zero, + onPing: JSC.JSValue = .zero, + onPong: JSC.JSValue = .zero, + + app: ?*anyopaque = null, + + // Always set manually. + vm: *JSC.VirtualMachine = undefined, + globalObject: *JSC.JSGlobalObject = undefined, + active_connections: usize = 0, + + /// used by publish() + flags: packed struct(u2) { + ssl: bool = false, + publish_to_self: bool = false, + } = .{}, + + pub fn runErrorCallback(this: *const Handler, vm: *JSC.VirtualMachine, globalObject: *JSC.JSGlobalObject, error_value: JSC.JSValue) void { + const onError = this.onError; + if (!onError.isEmptyOrUndefinedOrNull()) { + _ = onError.call(globalObject, .undefined, &.{error_value}) catch |err| + this.globalObject.reportActiveExceptionAsUnhandled(err); + return; + } + + _ = vm.uncaughtException(globalObject, error_value, false); + } + + pub fn fromJS(globalObject: *JSC.JSGlobalObject, object: JSC.JSValue) bun.JSError!Handler { + var handler = Handler{ .globalObject = globalObject, .vm = VirtualMachine.get() }; + + var valid = false; + + if (try object.getTruthyComptime(globalObject, "message")) |message_| { + if (!message_.isCallable()) { + return globalObject.throwInvalidArguments("websocket expects a function for the message option", .{}); + } + const message = message_.withAsyncContextIfNeeded(globalObject); + handler.onMessage = message; + message.ensureStillAlive(); + valid = true; + } + + if (try object.getTruthy(globalObject, "open")) |open_| { + if (!open_.isCallable()) { + return globalObject.throwInvalidArguments("websocket expects a function for the open option", .{}); + } + const open = open_.withAsyncContextIfNeeded(globalObject); + handler.onOpen = open; + open.ensureStillAlive(); + valid = true; + } + + if (try object.getTruthy(globalObject, "close")) |close_| { + if (!close_.isCallable()) { + return globalObject.throwInvalidArguments("websocket expects a function for the close option", .{}); + } + const close = close_.withAsyncContextIfNeeded(globalObject); + handler.onClose = close; + close.ensureStillAlive(); + valid = true; + } + + if (try object.getTruthy(globalObject, "drain")) |drain_| { + if (!drain_.isCallable()) { + return globalObject.throwInvalidArguments("websocket expects a function for the drain option", .{}); + } + const drain = drain_.withAsyncContextIfNeeded(globalObject); + handler.onDrain = drain; + drain.ensureStillAlive(); + valid = true; + } + + if (try object.getTruthy(globalObject, "onError")) |onError_| { + if (!onError_.isCallable()) { + return globalObject.throwInvalidArguments("websocket expects a function for the onError option", .{}); + } + const onError = onError_.withAsyncContextIfNeeded(globalObject); + handler.onError = onError; + onError.ensureStillAlive(); + } + + if (try object.getTruthy(globalObject, "ping")) |cb| { + if (!cb.isCallable()) { + return globalObject.throwInvalidArguments("websocket expects a function for the ping option", .{}); + } + handler.onPing = cb; + cb.ensureStillAlive(); + valid = true; + } + + if (try object.getTruthy(globalObject, "pong")) |cb| { + if (!cb.isCallable()) { + return globalObject.throwInvalidArguments("websocket expects a function for the pong option", .{}); + } + handler.onPong = cb; + cb.ensureStillAlive(); + valid = true; + } + + if (valid) + return handler; + + return globalObject.throwInvalidArguments("WebSocketServerContext expects a message handler", .{}); + } + + pub fn protect(this: Handler) void { + this.onOpen.protect(); + this.onMessage.protect(); + this.onClose.protect(); + this.onDrain.protect(); + this.onError.protect(); + this.onPing.protect(); + this.onPong.protect(); + } + + pub fn unprotect(this: Handler) void { + if (this.vm.isShuttingDown()) { + return; + } + + this.onOpen.unprotect(); + this.onMessage.unprotect(); + this.onClose.unprotect(); + this.onDrain.unprotect(); + this.onError.unprotect(); + this.onPing.unprotect(); + this.onPong.unprotect(); + } +}; + +pub fn toBehavior(this: WebSocketServerContext) uws.WebSocketBehavior { + return .{ + .maxPayloadLength = this.maxPayloadLength, + .idleTimeout = this.idleTimeout, + .compression = this.compression, + .maxBackpressure = this.backpressureLimit, + .sendPingsAutomatically = this.sendPingsAutomatically, + .maxLifetime = this.maxLifetime, + .resetIdleTimeoutOnSend = this.resetIdleTimeoutOnSend, + .closeOnBackpressureLimit = this.closeOnBackpressureLimit, + }; +} + +pub fn protect(this: WebSocketServerContext) void { + this.handler.protect(); +} +pub fn unprotect(this: WebSocketServerContext) void { + this.handler.unprotect(); +} + +const CompressTable = bun.ComptimeStringMap(i32, .{ + .{ "disable", 0 }, + .{ "shared", uws.SHARED_COMPRESSOR }, + .{ "dedicated", uws.DEDICATED_COMPRESSOR }, + .{ "3KB", uws.DEDICATED_COMPRESSOR_3KB }, + .{ "4KB", uws.DEDICATED_COMPRESSOR_4KB }, + .{ "8KB", uws.DEDICATED_COMPRESSOR_8KB }, + .{ "16KB", uws.DEDICATED_COMPRESSOR_16KB }, + .{ "32KB", uws.DEDICATED_COMPRESSOR_32KB }, + .{ "64KB", uws.DEDICATED_COMPRESSOR_64KB }, + .{ "128KB", uws.DEDICATED_COMPRESSOR_128KB }, + .{ "256KB", uws.DEDICATED_COMPRESSOR_256KB }, +}); + +const DecompressTable = bun.ComptimeStringMap(i32, .{ + .{ "disable", 0 }, + .{ "shared", uws.SHARED_DECOMPRESSOR }, + .{ "dedicated", uws.DEDICATED_DECOMPRESSOR }, + .{ "3KB", uws.DEDICATED_COMPRESSOR_3KB }, + .{ "4KB", uws.DEDICATED_COMPRESSOR_4KB }, + .{ "8KB", uws.DEDICATED_COMPRESSOR_8KB }, + .{ "16KB", uws.DEDICATED_COMPRESSOR_16KB }, + .{ "32KB", uws.DEDICATED_COMPRESSOR_32KB }, + .{ "64KB", uws.DEDICATED_COMPRESSOR_64KB }, + .{ "128KB", uws.DEDICATED_COMPRESSOR_128KB }, + .{ "256KB", uws.DEDICATED_COMPRESSOR_256KB }, +}); + +pub fn onCreate(globalObject: *JSC.JSGlobalObject, object: JSValue) bun.JSError!WebSocketServerContext { + var server = WebSocketServerContext{}; + server.handler = try Handler.fromJS(globalObject, object); + + if (try object.get(globalObject, "perMessageDeflate")) |per_message_deflate| { + getter: { + if (per_message_deflate.isUndefined()) { + break :getter; + } + + if (per_message_deflate.isBoolean() or per_message_deflate.isNull()) { + if (per_message_deflate.toBoolean()) { + server.compression = uws.SHARED_COMPRESSOR | uws.SHARED_DECOMPRESSOR; + } else { + server.compression = 0; + } + break :getter; + } + + if (try per_message_deflate.getTruthy(globalObject, "compress")) |compression| { + if (compression.isBoolean()) { + server.compression |= if (compression.toBoolean()) uws.SHARED_COMPRESSOR else 0; + } else if (compression.isString()) { + server.compression |= CompressTable.getWithEql(try compression.getZigString(globalObject), ZigString.eqlComptime) orelse { + return globalObject.throwInvalidArguments("WebSocketServerContext expects a valid compress option, either disable \"shared\" \"dedicated\" \"3KB\" \"4KB\" \"8KB\" \"16KB\" \"32KB\" \"64KB\" \"128KB\" or \"256KB\"", .{}); + }; + } else { + return globalObject.throwInvalidArguments("websocket expects a valid compress option, either disable \"shared\" \"dedicated\" \"3KB\" \"4KB\" \"8KB\" \"16KB\" \"32KB\" \"64KB\" \"128KB\" or \"256KB\"", .{}); + } + } + + if (try per_message_deflate.getTruthy(globalObject, "decompress")) |compression| { + if (compression.isBoolean()) { + server.compression |= if (compression.toBoolean()) uws.SHARED_DECOMPRESSOR else 0; + } else if (compression.isString()) { + server.compression |= DecompressTable.getWithEql(try compression.getZigString(globalObject), ZigString.eqlComptime) orelse { + return globalObject.throwInvalidArguments("websocket expects a valid decompress option, either \"disable\" \"shared\" \"dedicated\" \"3KB\" \"4KB\" \"8KB\" \"16KB\" \"32KB\" \"64KB\" \"128KB\" or \"256KB\"", .{}); + }; + } else { + return globalObject.throwInvalidArguments("websocket expects a valid decompress option, either \"disable\" \"shared\" \"dedicated\" \"3KB\" \"4KB\" \"8KB\" \"16KB\" \"32KB\" \"64KB\" \"128KB\" or \"256KB\"", .{}); + } + } + } + } + + if (try object.get(globalObject, "maxPayloadLength")) |value| { + if (!value.isUndefinedOrNull()) { + if (!value.isAnyInt()) { + return globalObject.throwInvalidArguments("websocket expects maxPayloadLength to be an integer", .{}); + } + server.maxPayloadLength = @truncate(@max(value.toInt64(), 0)); + } + } + + if (try object.get(globalObject, "idleTimeout")) |value| { + if (!value.isUndefinedOrNull()) { + if (!value.isAnyInt()) { + return globalObject.throwInvalidArguments("websocket expects idleTimeout to be an integer", .{}); + } + + var idleTimeout: u16 = @truncate(@max(value.toInt64(), 0)); + if (idleTimeout > 960) { + return globalObject.throwInvalidArguments("websocket expects idleTimeout to be 960 or less", .{}); + } else if (idleTimeout > 0) { + // uws does not allow idleTimeout to be between (0, 8), + // since its timer is not that accurate, therefore round up. + idleTimeout = @max(idleTimeout, 8); + } + + server.idleTimeout = idleTimeout; + } + } + if (try object.get(globalObject, "backpressureLimit")) |value| { + if (!value.isUndefinedOrNull()) { + if (!value.isAnyInt()) { + return globalObject.throwInvalidArguments("websocket expects backpressureLimit to be an integer", .{}); + } + + server.backpressureLimit = @truncate(@max(value.toInt64(), 0)); + } + } + + if (try object.get(globalObject, "closeOnBackpressureLimit")) |value| { + if (!value.isUndefinedOrNull()) { + if (!value.isBoolean()) { + return globalObject.throwInvalidArguments("websocket expects closeOnBackpressureLimit to be a boolean", .{}); + } + + server.closeOnBackpressureLimit = value.toBoolean(); + } + } + + if (try object.get(globalObject, "sendPings")) |value| { + if (!value.isUndefinedOrNull()) { + if (!value.isBoolean()) { + return globalObject.throwInvalidArguments("websocket expects sendPings to be a boolean", .{}); + } + + server.sendPingsAutomatically = value.toBoolean(); + } + } + + if (try object.get(globalObject, "publishToSelf")) |value| { + if (!value.isUndefinedOrNull()) { + if (!value.isBoolean()) { + return globalObject.throwInvalidArguments("websocket expects publishToSelf to be a boolean", .{}); + } + + server.handler.flags.publish_to_self = value.toBoolean(); + } + } + + server.protect(); + return server; +} + +const bun = @import("bun"); +const uws = bun.uws; +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const JSError = bun.JSError; +const VirtualMachine = JSC.VirtualMachine; +const ZigString = JSC.ZigString;