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); bun.default_allocator.free(this.base_uri); this.base_uri = ""; this.base_url = .{}; for (this.negative_routes.items) |route| { bun.default_allocator.free(route); } this.negative_routes.clearAndFree(); 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(); } sni.deinit(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 = bun.handleOom(duped_route_names.getOrPut(route_name)); 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.com/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, .user_routes = &args.static_routes, .global = global, }; const init_ctx: *AnyRoute.ServerInitContext = &init_ctx_; 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 = bun.handleOom(key.toOwnedSliceReturningAllASCII(bun.default_allocator)); 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.handleOom(bun.default_allocator.dupeZ(u8, path)); defer bun.default_allocator.free(path); bun.handleOom(args.negative_routes.append(duped)); continue; } if (value.isCallable()) { try validateRouteName(global, path); args.user_routes_to_build.append(.{ .route = .{ .path = bun.handleOom(bun.default_allocator.dupeZ(u8, path)), .method = .any, }, .callback = .create(value.withAsyncContextIfNeeded(global), global), }) catch |err| bun.handleOom(err); 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 (try 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.handleOom(bun.default_allocator.dupeZ(u8, path)), .method = .{ .specific = method }, }, .callback = .create(function.withAsyncContextIfNeeded(global), global), }) catch |err| bun.handleOom(err); } 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.handleOom(bun.default_allocator.dupe(u8, path)), .route = html_route, .method = .{ .method = method_set }, }) catch |err| bun.handleOom(err); } } } 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.com/docs/api/http for more information. , .{}, ); }; args.static_routes.append(.{ .path = path, .route = route, }) catch |err| bun.handleOom(err); } // 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, try 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.toBoolean(); } if (global.hasException()) return error.JSError; if (try arg.get(global, "ipv6Only")) |dev| { args.ipv6_only = dev.toBoolean(); } if (global.hasException()) return error.JSError; if (try arg.get(global, "inspector")) |inspector| { args.inspector = inspector.toBoolean(); 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.com/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 = try tls.arrayIterator(global); if (value_iter.len == 0) { // Empty TLS array means no TLS - this is valid } else { while (try 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 orelse "")[0] == 0) { defer ssl_config.deinit(); return global.throwInvalidArguments("SNI tls object must have a serverName", .{}); } if (args.sni == null) { args.sni = bun.handleOom(bun.BabyList(SSLConfig).initCapacity(bun.default_allocator, value_iter.len - 1)); } bun.handleOom(args.sni.?.append(bun.default_allocator, ssl_config)); } } } } 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 string = []const u8; const WebSocketServerContext = @import("./WebSocketServerContext.zig"); const std = @import("std"); const bun = @import("bun"); const HTTP = bun.http; const JSError = bun.JSError; const URL = bun.URL; const assert = bun.assert; const jsc = bun.jsc; const strings = bun.strings; const uws = bun.uws; const AnyRoute = bun.api.server.AnyRoute; const AnyServer = jsc.API.AnyServer;