Files
bun.sh/src/bun.js/api/server.zig
taylor.fish 07cd45deae Refactor Zig imports and file structure (part 1) (#21270)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-07-22 17:51:38 -07:00

3422 lines
144 KiB
Zig

const httplog = Output.scoped(.Server, false);
const ctxLog = Output.scoped(.RequestContext, false);
pub const WebSocketServerContext = @import("./server/WebSocketServerContext.zig");
pub const HTTPStatusText = @import("./server/HTTPStatusText.zig");
pub const HTMLBundle = @import("./server/HTMLBundle.zig");
pub fn writeStatus(comptime ssl: bool, resp_ptr: ?*uws.NewApp(ssl).Response, status: u16) void {
if (resp_ptr) |resp| {
if (HTTPStatusText.get(status)) |text| {
resp.writeStatus(text);
} else {
var status_text_buf: [48]u8 = undefined;
resp.writeStatus(std.fmt.bufPrint(&status_text_buf, "{d} HM", .{status}) catch unreachable);
}
}
}
// TODO: rename to StaticBlobRoute? the html bundle is sometimes a static route
pub const StaticRoute = @import("./server/StaticRoute.zig");
pub const FileRoute = @import("./server/FileRoute.zig");
pub const AnyRoute = union(enum) {
/// Serve a static file
/// "/robots.txt": new Response(...),
static: *StaticRoute,
/// Serve a file from disk
file: *FileRoute,
/// Bundle an HTML import
/// import html from "./index.html";
/// "/": html,
html: bun.ptr.RefPtr(HTMLBundle.Route),
/// Use file system routing.
/// "/*": {
/// "dir": import.meta.resolve("./pages"),
/// "style": "nextjs-pages",
/// }
framework_router: bun.bake.FrameworkRouter.Type.Index,
pub fn memoryCost(this: AnyRoute) usize {
return switch (this) {
.static => |static_route| static_route.memoryCost(),
.file => |file_route| file_route.memoryCost(),
.html => |html_bundle_route| html_bundle_route.data.memoryCost(),
.framework_router => @sizeOf(bun.bake.Framework.FileSystemRouterType),
};
}
pub fn setServer(this: AnyRoute, server: ?AnyServer) void {
switch (this) {
.static => |static_route| static_route.server = server,
.file => |file_route| file_route.server = server,
.html => |html_bundle_route| html_bundle_route.server = server,
.framework_router => {}, // DevServer contains .server field
}
}
pub fn deref(this: AnyRoute) void {
switch (this) {
.static => |static_route| static_route.deref(),
.file => |file_route| file_route.deref(),
.html => |html_bundle_route| html_bundle_route.deref(),
.framework_router => {}, // not reference counted
}
}
pub fn ref(this: AnyRoute) void {
switch (this) {
.static => |static_route| static_route.ref(),
.file => |file_route| file_route.ref(),
.html => |html_bundle_route| html_bundle_route.ref(),
.framework_router => {}, // not reference counted
}
}
fn bundledHTMLManifestItemFromJS(argument: jsc.JSValue, index_path: []const u8, init_ctx: *ServerInitContext) bun.JSError!?AnyRoute {
if (!argument.isObject()) return null;
const path_string = try bun.String.fromJS(try argument.get(init_ctx.global, "path") orelse return null, init_ctx.global);
defer path_string.deref();
var path = jsc.Node.PathOrFileDescriptor{ .path = try jsc.Node.PathLike.fromBunString(init_ctx.global, path_string, false, bun.default_allocator) };
defer path.deinit();
// Construct the route by stripping paths above the root.
//
// "./index-abc.js" -> "/index-abc.js"
// "../index-abc.js" -> "/index-abc.js"
// "/index-abc.js" -> "/index-abc.js"
// "index-abc.js" -> "/index-abc.js"
//
const cwd = if (bun.StandaloneModuleGraph.isBunStandaloneFilePath(path.path.slice()))
bun.StandaloneModuleGraph.targetBasePublicPath(bun.Environment.os, "root/")
else
bun.fs.FileSystem.instance.top_level_dir;
const abs_path = bun.fs.FileSystem.instance.abs(&[_][]const u8{path.path.slice()});
var relative_path = bun.fs.FileSystem.instance.relative(cwd, abs_path);
if (strings.hasPrefixComptime(relative_path, "./")) {
relative_path = relative_path[2..];
} else if (strings.hasPrefixComptime(relative_path, "../")) {
while (strings.hasPrefixComptime(relative_path, "../")) {
relative_path = relative_path[3..];
}
}
const is_index_route = bun.strings.eql(path.path.slice(), index_path);
var builder = std.ArrayList(u8).init(bun.default_allocator);
defer builder.deinit();
if (!strings.hasPrefixComptime(relative_path, "/")) {
try builder.append('/');
}
try builder.appendSlice(relative_path);
const fetch_headers = try jsc.WebCore.FetchHeaders.createFromJS(init_ctx.global, try argument.get(init_ctx.global, "headers") orelse return null);
defer if (fetch_headers) |headers| headers.deref();
if (init_ctx.global.hasException()) return error.JSError;
const route = try fromOptions(init_ctx.global, fetch_headers, &path);
if (is_index_route) {
return route;
}
var methods = HTTP.Method.Optional{ .method = .initEmpty() };
methods.insert(.GET);
methods.insert(.HEAD);
try init_ctx.user_routes.append(.{
.path = try builder.toOwnedSlice(),
.route = route,
.method = methods,
});
return null;
}
/// This is the JS representation of an HTMLImportManifest
///
/// See ./src/bundler/HTMLImportManifest.zig
fn bundledHTMLManifestFromJS(argument: jsc.JSValue, init_ctx: *ServerInitContext) bun.JSError!?AnyRoute {
if (!argument.isObject()) return null;
const index = try argument.getOptional(init_ctx.global, "index", ZigString.Slice) orelse return null;
defer index.deinit();
const files = try argument.getArray(init_ctx.global, "files") orelse return null;
var iter = try files.arrayIterator(init_ctx.global);
var html_route: ?AnyRoute = null;
while (try iter.next()) |file_entry| {
if (try bundledHTMLManifestItemFromJS(file_entry, index.slice(), init_ctx)) |item| {
html_route = item;
}
}
return html_route;
}
pub fn fromOptions(global: *jsc.JSGlobalObject, headers: ?*jsc.WebCore.FetchHeaders, path: *jsc.Node.PathOrFileDescriptor) !AnyRoute {
// The file/static route doesn't ref it.
var blob = Blob.findOrCreateFileFromPath(path, global, false);
if (blob.needsToReadFile()) {
// Throw a more helpful error upfront if the file does not exist.
//
// In production, you do NOT want to find out that all the assets
// are 404'ing when the user goes to the route. You want to find
// that out immediately so that the health check on startup fails
// and the process exits with a non-zero status code.
if (blob.store) |store| {
if (store.getPath()) |store_path| {
switch (bun.sys.existsAtType(bun.FD.cwd(), store_path)) {
.result => |file_type| {
if (file_type == .directory) {
return global.throwInvalidArguments("Bundled file {} cannot be a directory. You may want to configure --asset-naming or `naming` when bundling.", .{bun.fmt.quote(store_path)});
}
},
.err => {
return global.throwInvalidArguments("Bundled file {} not found. You may want to configure --asset-naming or `naming` when bundling.", .{bun.fmt.quote(store_path)});
},
}
}
}
return AnyRoute{ .file = FileRoute.initFromBlob(blob, .{ .server = null, .headers = headers }) };
}
return AnyRoute{ .static = StaticRoute.initFromAnyBlob(&.{ .Blob = blob }, .{ .server = null, .headers = headers }) };
}
pub fn htmlRouteFromJS(argument: jsc.JSValue, init_ctx: *ServerInitContext) bun.JSError!?AnyRoute {
if (argument.as(HTMLBundle)) |html_bundle| {
const entry = init_ctx.dedupe_html_bundle_map.getOrPut(html_bundle) catch bun.outOfMemory();
if (!entry.found_existing) {
entry.value_ptr.* = HTMLBundle.Route.init(html_bundle);
return .{ .html = entry.value_ptr.* };
} else {
return .{ .html = entry.value_ptr.dupeRef() };
}
}
if (try bundledHTMLManifestFromJS(argument, init_ctx)) |html_route| {
return html_route;
}
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,
global: *jsc.JSGlobalObject,
framework_router_list: std.ArrayList(bun.bake.Framework.FileSystemRouterType),
user_routes: *std.ArrayList(ServerConfig.StaticRouteEntry),
};
pub fn fromJS(
global: *jsc.JSGlobalObject,
path: []const u8,
argument: jsc.JSValue,
init_ctx: *ServerInitContext,
) bun.JSError!?AnyRoute {
if (try AnyRoute.htmlRouteFromJS(argument, init_ctx)) |html_route| {
return html_route;
}
if (argument.isObject()) {
const FrameworkRouter = bun.bake.FrameworkRouter;
if (try argument.getOptional(global, "dir", bun.String.Slice)) |dir| {
var alloc = init_ctx.js_string_allocations;
const relative_root = alloc.track(dir);
var style: FrameworkRouter.Style = if (try argument.get(global, "style")) |style|
try FrameworkRouter.Style.fromJS(style, global)
else
.nextjs_pages;
errdefer style.deinit();
if (!bun.strings.endsWith(path, "/*")) {
return global.throwInvalidArguments("To mount a directory, make sure the path ends in `/*`", .{});
}
try init_ctx.framework_router_list.append(.{
.root = relative_root,
.style = style,
// trim the /*
.prefix = if (path.len == 2) "/" else path[0 .. path.len - 2],
// TODO: customizable framework option.
.entry_client = "bun-framework-react/client.tsx",
.entry_server = "bun-framework-react/server.tsx",
.ignore_underscores = true,
.ignore_dirs = &.{ "node_modules", ".git" },
.extensions = &.{ ".tsx", ".jsx" },
.allow_layouts = true,
});
const limit = std.math.maxInt(@typeInfo(FrameworkRouter.Type.Index).@"enum".tag_type);
if (init_ctx.framework_router_list.items.len > limit) {
return global.throwInvalidArguments("Too many framework routers. Maximum is {d}.", .{limit});
}
return .{ .framework_router = .init(@intCast(init_ctx.framework_router_list.items.len - 1)) };
}
}
if (try FileRoute.fromJS(global, argument)) |file_route| {
return .{ .file = file_route };
}
return .{ .static = try StaticRoute.fromJS(global, argument) orelse return null };
}
};
pub const ServerConfig = @import("./server/ServerConfig.zig");
pub const ServerWebSocket = @import("./server/ServerWebSocket.zig");
pub const NodeHTTPResponse = @import("./server/NodeHTTPResponse.zig");
/// State machine to handle loading plugins asynchronously. This structure is not thread-safe.
const ServePlugins = struct {
state: State,
ref_count: RefCount,
/// Reference count is incremented while there are other objects that are waiting on plugin loads.
const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{});
pub const ref = RefCount.ref;
pub const deref = RefCount.deref;
pub const State = union(enum) {
unqueued: []const []const u8,
pending: struct {
/// Promise may be empty if the plugin load finishes synchronously.
plugin: *bun.jsc.API.JSBundler.Plugin,
promise: jsc.JSPromise.Strong,
html_bundle_routes: std.ArrayListUnmanaged(*HTMLBundle.Route),
dev_server: ?*bun.bake.DevServer,
},
loaded: *bun.jsc.API.JSBundler.Plugin,
/// Error information is not stored as it is already reported.
err,
};
pub const GetOrStartLoadResult = union(enum) {
/// null = no plugins, used by server implementation
ready: ?*bun.jsc.API.JSBundler.Plugin,
pending,
err,
};
pub const Callback = union(enum) {
html_bundle_route: *HTMLBundle.Route,
dev_server: *bun.bake.DevServer,
};
pub fn init(plugins: []const []const u8) *ServePlugins {
return bun.new(ServePlugins, .{ .ref_count = .init(), .state = .{ .unqueued = plugins } });
}
fn deinit(this: *ServePlugins) void {
switch (this.state) {
.unqueued => {},
.pending => assert(false), // should have one ref while pending!
.loaded => |loaded| loaded.deinit(),
.err => {},
}
bun.destroy(this);
}
pub fn getOrStartLoad(this: *ServePlugins, global: *jsc.JSGlobalObject, cb: Callback) bun.JSError!GetOrStartLoadResult {
sw: switch (this.state) {
.unqueued => {
try this.loadAndResolvePlugins(global);
continue :sw this.state; // could jump to any branch if synchronously resolved
},
.pending => |*pending| {
switch (cb) {
.html_bundle_route => |route| {
route.ref();
try pending.html_bundle_routes.append(bun.default_allocator, route);
},
.dev_server => |server| {
assert(pending.dev_server == null or pending.dev_server == server); // one dev server per server
pending.dev_server = server;
},
}
return .pending;
},
.loaded => |plugins| return .{ .ready = plugins },
.err => return .err,
}
}
extern fn JSBundlerPlugin__loadAndResolvePluginsForServe(
plugin: *bun.jsc.API.JSBundler.Plugin,
plugins: jsc.JSValue,
bunfig_folder: jsc.JSValue,
) JSValue;
fn loadAndResolvePlugins(this: *ServePlugins, global: *jsc.JSGlobalObject) bun.JSError!void {
bun.assert(this.state == .unqueued);
const plugin_list = this.state.unqueued;
const bunfig_folder = bun.path.dirname(global.bunVM().transpiler.options.bunfig_path, .auto);
this.ref();
defer this.deref();
const plugin = bun.jsc.API.JSBundler.Plugin.create(global, .browser);
var sfb = std.heap.stackFallback(@sizeOf(bun.String) * 4, bun.default_allocator);
const alloc = sfb.get();
const bunstring_array = alloc.alloc(bun.String, plugin_list.len) catch bun.outOfMemory();
defer alloc.free(bunstring_array);
for (plugin_list, bunstring_array) |raw_plugin, *out| {
out.* = bun.String.init(raw_plugin);
}
const plugin_js_array = try bun.String.toJSArray(global, bunstring_array);
const bunfig_folder_bunstr = try bun.String.createUTF8ForJS(global, bunfig_folder);
this.state = .{ .pending = .{
.promise = jsc.JSPromise.Strong.init(global),
.plugin = plugin,
.html_bundle_routes = .empty,
.dev_server = null,
} };
global.bunVM().eventLoop().enter();
const result = try bun.jsc.fromJSHostCall(global, @src(), JSBundlerPlugin__loadAndResolvePluginsForServe, .{ plugin, plugin_js_array, bunfig_folder_bunstr });
global.bunVM().eventLoop().exit();
// handle the case where js synchronously throws an error
if (global.tryTakeException()) |e| {
handleOnReject(this, global, e);
return;
}
if (!result.isEmptyOrUndefinedOrNull()) {
// handle the case where js returns a promise
if (result.asAnyPromise()) |promise| {
switch (promise.status(global.vm())) {
// promise not fulfilled yet
.pending => {
this.ref();
const promise_value = promise.asValue();
this.state.pending.promise.strong.set(global, promise_value);
promise_value.then(global, this, onResolveImpl, onRejectImpl);
return;
},
.fulfilled => {
handleOnResolve(this);
return;
},
.rejected => {
const value = promise.result(global.vm());
handleOnReject(this, global, value);
return;
},
}
}
if (result.toError()) |e| {
handleOnReject(this, global, e);
} else {
handleOnResolve(this);
}
}
}
pub const onResolve = jsc.toJSHostFn(onResolveImpl);
pub const onReject = jsc.toJSHostFn(onRejectImpl);
pub fn onResolveImpl(_: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
ctxLog("onResolve", .{});
const plugins_result, const plugins_js = callframe.argumentsAsArray(2);
var plugins = plugins_js.asPromisePtr(ServePlugins);
defer plugins.deref();
plugins_result.ensureStillAlive();
handleOnResolve(plugins);
return .js_undefined;
}
pub fn handleOnResolve(this: *ServePlugins) void {
bun.assert(this.state == .pending);
const pending = &this.state.pending;
const plugin = pending.plugin;
var html_bundle_routes = pending.html_bundle_routes;
pending.html_bundle_routes = .empty;
defer html_bundle_routes.deinit(bun.default_allocator);
pending.promise.deinit();
this.state = .{ .loaded = plugin };
for (html_bundle_routes.items) |route| {
route.onPluginsResolved(plugin) catch bun.outOfMemory();
route.deref();
}
if (pending.dev_server) |server| {
server.onPluginsResolved(plugin) catch bun.outOfMemory();
}
}
pub fn onRejectImpl(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
ctxLog("onReject", .{});
const error_js, const plugin_js = callframe.argumentsAsArray(2);
const plugins = plugin_js.asPromisePtr(ServePlugins);
handleOnReject(plugins, globalThis, error_js);
return .js_undefined;
}
pub fn handleOnReject(this: *ServePlugins, global: *jsc.JSGlobalObject, err: JSValue) void {
bun.assert(this.state == .pending);
const pending = &this.state.pending;
var html_bundle_routes = pending.html_bundle_routes;
pending.html_bundle_routes = .empty;
defer html_bundle_routes.deinit(bun.default_allocator);
pending.plugin.deinit();
pending.promise.deinit();
this.state = .err;
for (html_bundle_routes.items) |route| {
route.onPluginsRejected() catch bun.outOfMemory();
route.deref();
}
if (pending.dev_server) |server| {
server.onPluginsRejected() catch bun.outOfMemory();
}
Output.errGeneric("Failed to load plugins for Bun.serve:", .{});
global.bunVM().runErrorHandler(err, null);
}
comptime {
@export(&onResolve, .{ .name = "BunServe__onResolvePlugins" });
@export(&onReject, .{ .name = "BunServe__onRejectPlugins" });
}
};
const PluginsResult = union(enum) {
pending,
found: ?*bun.jsc.API.JSBundler.Plugin,
err,
};
pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { debug, production }) type {
return struct {
pub const js = switch (protocol_enum) {
.http => switch (development_kind) {
.debug => bun.jsc.Codegen.JSDebugHTTPServer,
.production => bun.jsc.Codegen.JSHTTPServer,
},
.https => switch (development_kind) {
.debug => bun.jsc.Codegen.JSDebugHTTPSServer,
.production => bun.jsc.Codegen.JSHTTPSServer,
},
};
pub const fromJS = js.fromJS;
pub const toJS = js.toJS;
pub const toJSDirect = js.toJSDirect;
pub const new = bun.TrivialNew(@This());
pub const ssl_enabled = protocol_enum == .https;
pub const debug_mode = development_kind == .debug;
const ThisServer = @This();
pub const RequestContext = NewRequestContext(ssl_enabled, debug_mode, @This());
pub const App = uws.NewApp(ssl_enabled);
app: ?*App = null,
listener: ?*App.ListenSocket = null,
js_value: jsc.Strong.Optional = .empty,
/// Potentially null before listen() is called, and once .destroy() is called.
vm: *jsc.VirtualMachine,
globalThis: *JSGlobalObject,
base_url_string_for_joining: string = "",
config: ServerConfig = ServerConfig{},
pending_requests: usize = 0,
request_pool_allocator: *RequestContext.RequestContextStackAllocator = undefined,
all_closed_promise: jsc.JSPromise.Strong = .{},
listen_callback: jsc.AnyTask = undefined,
allocator: std.mem.Allocator,
poll_ref: Async.KeepAlive = .{},
cached_hostname: bun.String = bun.String.empty,
flags: packed struct(u4) {
deinit_scheduled: bool = false,
terminated: bool = false,
has_js_deinited: bool = false,
has_handled_all_closed_promise: bool = false,
} = .{},
plugins: ?*ServePlugins = null,
dev_server: ?*bun.bake.DevServer,
/// These associate a route to the index in RouteList.cpp.
/// User routes may get applied multiple times due to SNI.
/// So we have to store it.
user_routes: std.ArrayListUnmanaged(UserRoute) = .{},
on_clienterror: jsc.Strong.Optional = .empty,
inspector_server_id: jsc.Debugger.DebuggerId = .init(0),
pub const doStop = host_fn.wrapInstanceMethod(ThisServer, "stopFromJS", false);
pub const dispose = host_fn.wrapInstanceMethod(ThisServer, "disposeFromJS", false);
pub const doUpgrade = host_fn.wrapInstanceMethod(ThisServer, "onUpgrade", false);
pub const doPublish = host_fn.wrapInstanceMethod(ThisServer, "publish", false);
pub const doReload = onReload;
pub const doFetch = onFetch;
pub const doRequestIP = host_fn.wrapInstanceMethod(ThisServer, "requestIP", false);
pub const doTimeout = timeout;
pub const UserRoute = struct {
id: u32,
server: *ThisServer,
route: ServerConfig.RouteDeclaration,
pub fn deinit(this: *UserRoute) void {
this.route.deinit();
}
};
/// Returns:
/// - .ready if no plugin has to be loaded
/// - .err if there is a cached failure. Currently, this requires restarting the entire server.
/// - .pending if `callback` was stored. It will call `onPluginsResolved` or `onPluginsRejected` later.
pub fn getOrLoadPlugins(server: *ThisServer, callback: ServePlugins.Callback) ServePlugins.GetOrStartLoadResult {
if (server.plugins) |p| {
return p.getOrStartLoad(server.globalThis, callback) catch bun.outOfMemory();
}
// no plugins
return .{ .ready = null };
}
pub fn doSubscriberCount(this: *ThisServer, globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const arguments = callframe.arguments_old(1);
if (arguments.len < 1) {
return globalThis.throwNotEnoughArguments("subscriberCount", 1, 0);
}
if (arguments.ptr[0].isEmptyOrUndefinedOrNull()) {
return globalThis.throwInvalidArguments("subscriberCount requires a topic name as a string", .{});
}
var topic = try arguments.ptr[0].toSlice(globalThis, bun.default_allocator);
defer topic.deinit();
if (topic.len == 0) {
return JSValue.jsNumber(0);
}
return JSValue.jsNumber((this.app.?.numSubscribers(topic.slice())));
}
pub fn constructor(globalThis: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!*ThisServer {
return globalThis.throw2("Server() is not a constructor", .{});
}
pub fn jsValueAssertAlive(server: *ThisServer) jsc.JSValue {
bun.debugAssert(server.listener != null); // this assertion is only valid while listening
return server.js_value.get() orelse brk: {
bun.debugAssert(false);
break :brk .js_undefined; // safe-ish
};
}
pub fn requestIP(this: *ThisServer, request: *jsc.WebCore.Request) bun.JSError!jsc.JSValue {
if (this.config.address == .unix) return JSValue.jsNull();
const info = request.request_context.getRemoteSocketInfo() orelse return JSValue.jsNull();
return SocketAddress.createDTO(this.globalThis, info.ip, @intCast(info.port), info.is_ipv6);
}
pub fn memoryCost(this: *ThisServer) usize {
return @sizeOf(ThisServer) +
this.base_url_string_for_joining.len +
this.config.memoryCost() +
(if (this.dev_server) |dev| dev.memoryCost() else 0);
}
pub fn timeout(this: *ThisServer, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const arguments = callframe.arguments_old(2).slice();
if (arguments.len < 2 or arguments[0].isEmptyOrUndefinedOrNull()) {
return globalObject.throwNotEnoughArguments("timeout", 2, arguments.len);
}
const seconds = arguments[1];
if (this.config.address == .unix) {
return JSValue.jsNull();
}
if (!seconds.isNumber()) {
return this.globalThis.throw("timeout() requires a number", .{});
}
const value = seconds.to(c_uint);
if (arguments[0].as(Request)) |request| {
_ = request.request_context.setTimeout(value);
} else if (arguments[0].as(NodeHTTPResponse)) |response| {
response.setTimeout(@truncate(value % 255));
} else {
return this.globalThis.throwInvalidArguments("timeout() requires a Request object", .{});
}
return .js_undefined;
}
pub fn setIdleTimeout(this: *ThisServer, seconds: c_uint) void {
this.config.idleTimeout = @truncate(@min(seconds, 255));
}
pub fn setFlags(this: *ThisServer, require_host_header: bool, use_strict_method_validation: bool) void {
if (this.app) |app| {
app.setFlags(require_host_header, use_strict_method_validation);
}
}
pub fn setMaxHTTPHeaderSize(this: *ThisServer, max_header_size: u64) void {
if (this.app) |app| {
app.setMaxHTTPHeaderSize(max_header_size);
}
}
pub fn appendStaticRoute(this: *ThisServer, path: []const u8, route: AnyRoute, method: HTTP.Method.Optional) !void {
try this.config.appendStaticRoute(path, route, method);
}
pub fn publish(this: *ThisServer, globalThis: *jsc.JSGlobalObject, topic: ZigString, message_value: JSValue, compress_value: ?JSValue) bun.JSError!JSValue {
if (this.config.websocket == null)
return JSValue.jsNumber(0);
const app = this.app.?;
if (topic.len == 0) {
httplog("publish() topic invalid", .{});
return globalThis.throw("publish requires a topic string", .{});
}
var topic_slice = topic.toSlice(bun.default_allocator);
defer topic_slice.deinit();
if (topic_slice.len == 0) {
return globalThis.throw("publish requires a non-empty topic", .{});
}
const compress = (compress_value orelse JSValue.jsBoolean(true)).toBoolean();
if (message_value.asArrayBuffer(globalThis)) |buffer| {
return JSValue.jsNumber(
// if 0, return 0
// else return number of bytes sent
@as(i32, @intFromBool(uws.AnyWebSocket.publishWithOptions(ssl_enabled, app, topic_slice.slice(), buffer.slice(), .binary, compress))) * @as(i32, @intCast(@as(u31, @truncate(buffer.len)))),
);
}
{
var js_string = message_value.toString(globalThis);
if (globalThis.hasException()) {
return .zero;
}
const view = js_string.view(globalThis);
const slice = view.toSlice(bun.default_allocator);
defer slice.deinit();
defer js_string.ensureStillAlive();
const buffer = slice.slice();
return JSValue.jsNumber(
// if 0, return 0
// else return number of bytes sent
@as(i32, @intFromBool(uws.AnyWebSocket.publishWithOptions(ssl_enabled, app, topic_slice.slice(), buffer, .text, compress))) * @as(i32, @intCast(@as(u31, @truncate(buffer.len)))),
);
}
}
pub fn onUpgrade(
this: *ThisServer,
globalThis: *jsc.JSGlobalObject,
object: jsc.JSValue,
optional: ?JSValue,
) bun.JSError!JSValue {
if (this.config.websocket == null) {
return globalThis.throwInvalidArguments("To enable websocket support, set the \"websocket\" object in Bun.serve({})", .{});
}
if (this.flags.terminated) {
return JSValue.jsBoolean(false);
}
if (object.as(NodeHTTPResponse)) |nodeHttpResponse| {
if (nodeHttpResponse.flags.ended or nodeHttpResponse.flags.socket_closed) {
return jsc.jsBoolean(false);
}
var data_value = jsc.JSValue.zero;
// if we converted a HeadersInit to a Headers object, we need to free it
var fetch_headers_to_deref: ?*WebCore.FetchHeaders = null;
defer {
if (fetch_headers_to_deref) |fh| {
fh.deref();
}
}
var sec_websocket_protocol = ZigString.Empty;
var sec_websocket_extensions = ZigString.Empty;
if (optional) |opts| {
getter: {
if (opts.isEmptyOrUndefinedOrNull()) {
break :getter;
}
if (!opts.isObject()) {
return globalThis.throwInvalidArguments("upgrade options must be an object", .{});
}
if (try opts.fastGet(globalThis, .data)) |headers_value| {
data_value = headers_value;
}
if (globalThis.hasException()) {
return error.JSError;
}
if (try opts.fastGet(globalThis, .headers)) |headers_value| {
if (headers_value.isEmptyOrUndefinedOrNull()) {
break :getter;
}
var fetch_headers_to_use: *WebCore.FetchHeaders = headers_value.as(WebCore.FetchHeaders) orelse brk: {
if (headers_value.isObject()) {
if (try WebCore.FetchHeaders.createFromJS(globalThis, headers_value)) |fetch_headers| {
fetch_headers_to_deref = fetch_headers;
break :brk fetch_headers;
}
}
break :brk null;
} orelse {
if (!globalThis.hasException()) {
return globalThis.throwInvalidArguments("upgrade options.headers must be a Headers or an object", .{});
}
return error.JSError;
};
if (globalThis.hasException()) {
return error.JSError;
}
if (fetch_headers_to_use.fastGet(.SecWebSocketProtocol)) |protocol| {
sec_websocket_protocol = protocol;
}
if (fetch_headers_to_use.fastGet(.SecWebSocketExtensions)) |protocol| {
sec_websocket_extensions = protocol;
}
// we must write the status first so that 200 OK isn't written
nodeHttpResponse.raw_response.writeStatus("101 Switching Protocols");
fetch_headers_to_use.toUWSResponse(comptime ssl_enabled, nodeHttpResponse.raw_response.socket());
}
if (globalThis.hasException()) {
return error.JSError;
}
}
}
return jsc.jsBoolean(nodeHttpResponse.upgrade(data_value, sec_websocket_protocol, sec_websocket_extensions));
}
var request = object.as(Request) orelse {
return globalThis.throwInvalidArguments("upgrade requires a Request object", .{});
};
var upgrader = request.request_context.get(RequestContext) orelse return jsc.jsBoolean(false);
if (upgrader.isAbortedOrEnded()) {
return jsc.jsBoolean(false);
}
if (upgrader.upgrade_context == null or @intFromPtr(upgrader.upgrade_context) == std.math.maxInt(usize)) {
return jsc.jsBoolean(false);
}
const resp = upgrader.resp.?;
const ctx = upgrader.upgrade_context.?;
var sec_websocket_key_str = ZigString.Empty;
var sec_websocket_protocol = ZigString.Empty;
var sec_websocket_extensions = ZigString.Empty;
if (request.getFetchHeaders()) |head| {
sec_websocket_key_str = head.fastGet(.SecWebSocketKey) orelse ZigString.Empty;
sec_websocket_protocol = head.fastGet(.SecWebSocketProtocol) orelse ZigString.Empty;
sec_websocket_extensions = head.fastGet(.SecWebSocketExtensions) orelse ZigString.Empty;
}
if (upgrader.req) |req| {
if (sec_websocket_key_str.len == 0) {
sec_websocket_key_str = ZigString.init(req.header("sec-websocket-key") orelse "");
}
if (sec_websocket_protocol.len == 0) {
sec_websocket_protocol = ZigString.init(req.header("sec-websocket-protocol") orelse "");
}
if (sec_websocket_extensions.len == 0) {
sec_websocket_extensions = ZigString.init(req.header("sec-websocket-extensions") orelse "");
}
}
if (sec_websocket_key_str.len == 0) {
return jsc.jsBoolean(false);
}
if (sec_websocket_protocol.len > 0) {
sec_websocket_protocol.markUTF8();
}
if (sec_websocket_extensions.len > 0) {
sec_websocket_extensions.markUTF8();
}
var data_value = jsc.JSValue.zero;
// if we converted a HeadersInit to a Headers object, we need to free it
var fetch_headers_to_deref: ?*WebCore.FetchHeaders = null;
defer {
if (fetch_headers_to_deref) |fh| {
fh.deref();
}
}
if (optional) |opts| {
getter: {
if (opts.isEmptyOrUndefinedOrNull()) {
break :getter;
}
if (!opts.isObject()) {
return globalThis.throwInvalidArguments("upgrade options must be an object", .{});
}
if (try opts.fastGet(globalThis, .data)) |headers_value| {
data_value = headers_value;
}
if (globalThis.hasException()) {
return error.JSError;
}
if (try opts.fastGet(globalThis, .headers)) |headers_value| {
if (headers_value.isEmptyOrUndefinedOrNull()) {
break :getter;
}
var fetch_headers_to_use: *WebCore.FetchHeaders = headers_value.as(WebCore.FetchHeaders) orelse brk: {
if (headers_value.isObject()) {
if (try WebCore.FetchHeaders.createFromJS(globalThis, headers_value)) |fetch_headers| {
fetch_headers_to_deref = fetch_headers;
break :brk fetch_headers;
}
}
break :brk null;
} orelse {
if (!globalThis.hasException()) {
return globalThis.throwInvalidArguments("upgrade options.headers must be a Headers or an object", .{});
}
return error.JSError;
};
if (globalThis.hasException()) {
return error.JSError;
}
if (fetch_headers_to_use.fastGet(.SecWebSocketProtocol)) |protocol| {
sec_websocket_protocol = protocol;
}
if (fetch_headers_to_use.fastGet(.SecWebSocketExtensions)) |protocol| {
sec_websocket_extensions = protocol;
}
// we must write the status first so that 200 OK isn't written
resp.writeStatus("101 Switching Protocols");
fetch_headers_to_use.toUWSResponse(comptime ssl_enabled, resp);
}
if (globalThis.hasException()) {
return error.JSError;
}
}
}
// --- After this point, do not throw an exception
// See https://github.com/oven-sh/bun/issues/1339
// obviously invalid pointer marks it as used
upgrader.upgrade_context = @as(*uws.SocketContext, @ptrFromInt(std.math.maxInt(usize)));
const signal = upgrader.signal;
upgrader.signal = null;
upgrader.resp = null;
request.request_context = AnyRequestContext.Null;
upgrader.request_weakref.deref();
data_value.ensureStillAlive();
const ws = ServerWebSocket.new(.{
.handler = &this.config.websocket.?.handler,
.this_value = data_value,
.signal = signal,
});
data_value.ensureStillAlive();
var sec_websocket_protocol_str = sec_websocket_protocol.toSlice(bun.default_allocator);
defer sec_websocket_protocol_str.deinit();
var sec_websocket_extensions_str = sec_websocket_extensions.toSlice(bun.default_allocator);
defer sec_websocket_extensions_str.deinit();
resp.clearAborted();
resp.clearOnData();
resp.clearOnWritable();
resp.clearTimeout();
upgrader.deref();
_ = resp.upgrade(
*ServerWebSocket,
ws,
sec_websocket_key_str.slice(),
sec_websocket_protocol_str.slice(),
sec_websocket_extensions_str.slice(),
ctx,
);
return jsc.jsBoolean(true);
}
pub fn onReloadFromZig(this: *ThisServer, new_config: *ServerConfig, globalThis: *jsc.JSGlobalObject) void {
httplog("onReload", .{});
this.app.?.clearRoutes();
// only reload those two, but ignore if they're not specified.
if (this.config.onRequest != new_config.onRequest and (new_config.onRequest != .zero and !new_config.onRequest.isUndefined())) {
this.config.onRequest.unprotect();
this.config.onRequest = new_config.onRequest;
}
if (this.config.onNodeHTTPRequest != new_config.onNodeHTTPRequest) {
this.config.onNodeHTTPRequest.unprotect();
this.config.onNodeHTTPRequest = new_config.onNodeHTTPRequest;
}
if (this.config.onError != new_config.onError and (new_config.onError != .zero and !new_config.onError.isUndefined())) {
this.config.onError.unprotect();
this.config.onError = new_config.onError;
}
if (new_config.websocket) |*ws| {
ws.handler.flags.ssl = ssl_enabled;
if (ws.handler.onMessage != .zero or ws.handler.onOpen != .zero) {
if (this.config.websocket) |old_ws| {
old_ws.unprotect();
}
ws.globalObject = globalThis;
this.config.websocket = ws.*;
} // we don't remove it
}
// These get re-applied when we set the static routes again.
if (this.dev_server) |dev_server| {
// Prevent a use-after-free in the hash table keys.
dev_server.html_router.clear();
dev_server.html_router.fallback = null;
}
var static_routes = this.config.static_routes;
this.config.static_routes = .init(bun.default_allocator);
for (static_routes.items) |*route| {
route.deinit();
}
static_routes.deinit();
this.config.static_routes = new_config.static_routes;
for (this.config.negative_routes.items) |route| {
bun.default_allocator.free(route);
}
this.config.negative_routes.clearAndFree();
this.config.negative_routes = new_config.negative_routes;
if (new_config.had_routes_object) {
for (this.config.user_routes_to_build.items) |*route| {
route.deinit();
}
this.config.user_routes_to_build.clearAndFree();
this.config.user_routes_to_build = new_config.user_routes_to_build;
for (this.user_routes.items) |*route| {
route.deinit();
}
this.user_routes.clearAndFree(bun.default_allocator);
}
const route_list_value = this.setRoutes();
if (new_config.had_routes_object) {
if (this.js_value.get()) |server_js_value| {
js.routeListSetCached(server_js_value, this.globalThis, route_list_value);
}
}
if (this.inspector_server_id.toOptional().unwrap() != null) {
if (this.vm.debugger) |*debugger| {
debugger.http_server_agent.notifyServerRoutesUpdated(
AnyServer.from(this),
) catch bun.outOfMemory();
}
}
}
pub fn reloadStaticRoutes(this: *ThisServer) !bool {
if (this.app == null) {
// Static routes will get cleaned up when the server is stopped
return false;
}
this.config = try this.config.cloneForReloadingStaticRoutes();
this.app.?.clearRoutes();
const route_list_value = this.setRoutes();
if (route_list_value != .zero) {
if (this.js_value.get()) |server_js_value| {
js.routeListSetCached(server_js_value, this.globalThis, route_list_value);
}
}
return true;
}
pub fn onReload(this: *ThisServer, globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const arguments = callframe.arguments();
if (arguments.len < 1) {
return globalThis.throwNotEnoughArguments("reload", 1, 0);
}
var args_slice = jsc.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), arguments);
defer args_slice.deinit();
var new_config: ServerConfig = .{};
try ServerConfig.fromJS(globalThis, &new_config, &args_slice, .{
.allow_bake_config = false,
.is_fetch_required = true,
.has_user_routes = this.user_routes.items.len > 0,
});
if (globalThis.hasException()) {
new_config.deinit();
return error.JSError;
}
this.onReloadFromZig(&new_config, globalThis);
return this.js_value.get() orelse .js_undefined;
}
pub fn onFetch(
this: *ThisServer,
ctx: *jsc.JSGlobalObject,
callframe: *jsc.CallFrame,
) bun.JSError!jsc.JSValue {
jsc.markBinding(@src());
if (this.config.onRequest == .zero) {
return JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(ctx, ZigString.init("fetch() requires the server to have a fetch handler").toErrorInstance(ctx));
}
const arguments = callframe.arguments_old(2).slice();
if (arguments.len == 0) {
const fetch_error = WebCore.Fetch.fetch_error_no_args;
return JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(ctx, ZigString.init(fetch_error).toErrorInstance(ctx));
}
var headers: ?*WebCore.FetchHeaders = null;
var method = HTTP.Method.GET;
var args = jsc.CallFrame.ArgumentsSlice.init(ctx.bunVM(), arguments);
defer args.deinit();
var first_arg = args.nextEat().?;
var body: jsc.WebCore.Body.Value = .{ .Null = {} };
var existing_request: WebCore.Request = undefined;
// TODO: set Host header
// TODO: set User-Agent header
// TODO: unify with fetch() implementation.
if (first_arg.isString()) {
const url_zig_str = try arguments[0].toSlice(ctx, bun.default_allocator);
defer url_zig_str.deinit();
var temp_url_str = url_zig_str.slice();
if (temp_url_str.len == 0) {
const fetch_error = jsc.WebCore.Fetch.fetch_error_blank_url;
return JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(ctx, ZigString.init(fetch_error).toErrorInstance(ctx));
}
var url = URL.parse(temp_url_str);
if (url.hostname.len == 0) {
url = URL.parse(
strings.append(this.allocator, this.base_url_string_for_joining, url.pathname) catch unreachable,
);
} else {
temp_url_str = this.allocator.dupe(u8, temp_url_str) catch unreachable;
url = URL.parse(temp_url_str);
}
if (arguments.len >= 2 and arguments[1].isObject()) {
var opts = arguments[1];
if (try opts.fastGet(ctx, .method)) |method_| {
var slice_ = try method_.toSlice(ctx, bun.default_allocator);
defer slice_.deinit();
method = HTTP.Method.which(slice_.slice()) orelse method;
}
if (try opts.fastGet(ctx, .headers)) |headers_| {
if (headers_.as(WebCore.FetchHeaders)) |headers__| {
headers = headers__;
} else if (try WebCore.FetchHeaders.createFromJS(ctx, headers_)) |headers__| {
headers = headers__;
}
}
if (try opts.fastGet(ctx, .body)) |body__| {
if (Blob.get(ctx, body__, true, false)) |new_blob| {
body = .{ .Blob = new_blob };
} else |_| {
return JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(ctx, ZigString.init("fetch() received invalid body").toErrorInstance(ctx));
}
}
}
existing_request = Request.init(
bun.String.cloneUTF8(url.href),
headers,
this.vm.initRequestBodyValue(body) catch bun.outOfMemory(),
method,
);
} else if (first_arg.as(Request)) |request_| {
try request_.cloneInto(
&existing_request,
bun.default_allocator,
ctx,
false,
);
} else {
const fetch_error = jsc.WebCore.Fetch.fetch_type_error_strings.get(bun.jsc.C.JSValueGetType(ctx, first_arg.asRef()));
const err = ctx.toTypeError(.INVALID_ARG_TYPE, "{s}", .{fetch_error});
return JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(ctx, err);
}
var request = Request.new(existing_request);
bun.assert(this.config.onRequest != .zero); // confirmed above
const response_value = this.config.onRequest.call(
this.globalThis,
this.jsValueAssertAlive(),
&[_]jsc.JSValue{request.toJS(this.globalThis)},
) catch |err| this.globalThis.takeException(err);
if (response_value.isAnyError()) {
return jsc.JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(ctx, response_value);
}
if (response_value.isEmptyOrUndefinedOrNull()) {
return jsc.JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(ctx, ZigString.init("fetch() returned an empty value").toErrorInstance(ctx));
}
if (response_value.asAnyPromise() != null) {
return response_value;
}
if (response_value.as(jsc.WebCore.Response)) |resp| {
resp.url = existing_request.url.clone();
}
return jsc.JSPromise.resolvedPromiseValue(ctx, response_value);
}
pub fn stopFromJS(this: *ThisServer, abruptly: ?JSValue) jsc.JSValue {
const rc = this.getAllClosedPromise(this.globalThis);
if (this.listener != null) {
const abrupt = brk: {
if (abruptly) |val| {
if (val.isBoolean() and val.toBoolean()) {
break :brk true;
}
}
break :brk false;
};
this.stop(abrupt);
}
return rc;
}
pub fn disposeFromJS(this: *ThisServer) jsc.JSValue {
if (this.listener != null) {
this.stop(true);
}
return .js_undefined;
}
pub fn getPort(
this: *ThisServer,
_: *jsc.JSGlobalObject,
) jsc.JSValue {
switch (this.config.address) {
.unix => return .js_undefined,
else => {},
}
var listener = this.listener orelse return jsc.JSValue.jsNumber(this.config.address.tcp.port);
return jsc.JSValue.jsNumber(listener.getLocalPort());
}
pub fn getId(this: *ThisServer, globalThis: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue {
return bun.String.createUTF8ForJS(globalThis, this.config.id);
}
pub fn getPendingRequests(this: *ThisServer, _: *jsc.JSGlobalObject) jsc.JSValue {
return jsc.JSValue.jsNumber(@as(i32, @intCast(@as(u31, @truncate(this.pending_requests)))));
}
pub fn getPendingWebSockets(this: *ThisServer, _: *jsc.JSGlobalObject) jsc.JSValue {
return jsc.JSValue.jsNumber(@as(i32, @intCast(@as(u31, @truncate(this.activeSocketsCount())))));
}
pub fn getAddress(this: *ThisServer, globalThis: *JSGlobalObject) jsc.JSValue {
switch (this.config.address) {
.unix => |unix| {
var value = bun.String.cloneUTF8(unix);
defer value.deref();
return value.toJS(globalThis);
},
.tcp => {
var port: u16 = this.config.address.tcp.port;
if (this.listener) |listener| {
port = @intCast(listener.getLocalPort());
var buf: [64]u8 = [_]u8{0} ** 64;
const address_bytes = listener.socket().localAddress(&buf) orelse return JSValue.jsNull();
var addr = SocketAddress.init(address_bytes, port) catch {
@branchHint(.unlikely);
return JSValue.jsNull();
};
return addr.intoDTO(this.globalThis);
}
return JSValue.jsNull();
},
}
}
pub fn getURLAsString(this: *const ThisServer) bun.OOM!bun.String {
const fmt = switch (this.config.address) {
.unix => |unix| brk: {
if (unix.len > 1 and unix[0] == 0) {
// abstract domain socket, let's give it an "abstract" URL
break :brk bun.fmt.URLFormatter{
.proto = .abstract,
.hostname = unix[1..],
};
}
break :brk bun.fmt.URLFormatter{
.proto = .unix,
.hostname = unix,
};
},
.tcp => |tcp| blk: {
var port: u16 = tcp.port;
if (this.listener) |listener| {
port = @intCast(listener.getLocalPort());
}
break :blk bun.fmt.URLFormatter{
.proto = if (comptime ssl_enabled) .https else .http,
.hostname = if (tcp.hostname) |hostname| bun.sliceTo(@constCast(hostname), 0) else null,
.port = port,
};
},
};
const buf = try std.fmt.allocPrint(default_allocator, "{any}", .{fmt});
defer default_allocator.free(buf);
return bun.String.cloneUTF8(buf);
}
pub fn getURL(this: *ThisServer, globalThis: *JSGlobalObject) bun.OOM!jsc.JSValue {
var url = try this.getURLAsString();
defer url.deref();
return url.toJSDOMURL(globalThis);
}
pub fn getHostname(this: *ThisServer, globalThis: *JSGlobalObject) jsc.JSValue {
switch (this.config.address) {
.unix => return .js_undefined,
else => {},
}
if (this.cached_hostname.isEmpty()) {
if (this.listener) |listener| {
var buf: [1024]u8 = [_]u8{0} ** 1024;
if (listener.socket().remoteAddress(buf[0..1024])) |addr| {
if (addr.len > 0) {
this.cached_hostname = bun.String.cloneUTF8(addr);
}
}
}
if (this.cached_hostname.isEmpty()) {
switch (this.config.address) {
.tcp => |tcp| {
if (tcp.hostname) |hostname| {
this.cached_hostname = bun.String.cloneUTF8(bun.sliceTo(hostname, 0));
} else {
this.cached_hostname = bun.String.createAtomASCII("localhost");
}
},
else => {},
}
}
}
return this.cached_hostname.toJS(globalThis);
}
pub fn getProtocol(this: *ThisServer, globalThis: *JSGlobalObject) jsc.JSValue {
_ = this;
return bun.String.static(if (ssl_enabled) "https" else "http").toJS(globalThis);
}
pub fn getDevelopment(
_: *ThisServer,
_: *jsc.JSGlobalObject,
) jsc.JSValue {
return jsc.JSValue.jsBoolean(debug_mode);
}
pub fn onStaticRequestComplete(this: *ThisServer) void {
this.pending_requests -= 1;
this.deinitIfWeCan();
}
pub fn onRequestComplete(this: *ThisServer) void {
this.vm.eventLoop().processGCTimer();
this.pending_requests -= 1;
this.deinitIfWeCan();
}
pub fn finalize(this: *ThisServer) void {
httplog("finalize", .{});
this.flags.has_js_deinited = true;
this.deinitIfWeCan();
}
pub fn activeSocketsCount(this: *const ThisServer) u32 {
const websocket = &(this.config.websocket orelse return 0);
return @as(u32, @truncate(websocket.handler.active_connections));
}
pub fn hasActiveWebSockets(this: *const ThisServer) bool {
return this.activeSocketsCount() > 0;
}
pub fn getAllClosedPromise(this: *ThisServer, globalThis: *jsc.JSGlobalObject) jsc.JSValue {
if (this.listener == null and this.pending_requests == 0) {
return jsc.JSPromise.resolvedPromise(globalThis, .js_undefined).toJS();
}
const prom = &this.all_closed_promise;
if (prom.strong.has()) {
return prom.value();
}
prom.* = jsc.JSPromise.Strong.init(globalThis);
return prom.value();
}
pub fn deinitIfWeCan(this: *ThisServer) void {
if (Environment.enable_logs)
httplog("deinitIfWeCan. requests={d}, listener={s}, websockets={s}, has_handled_all_closed_promise={}, all_closed_promise={s}, has_js_deinited={}", .{
this.pending_requests,
if (this.listener == null) "null" else "some",
if (this.hasActiveWebSockets()) "active" else "no",
this.flags.has_handled_all_closed_promise,
if (this.all_closed_promise.strong.has()) "has" else "no",
this.flags.has_js_deinited,
});
const vm = this.globalThis.bunVM();
if (this.pending_requests == 0 and
this.listener == null and
!this.hasActiveWebSockets() and
!this.flags.has_handled_all_closed_promise and
this.all_closed_promise.strong.has())
{
httplog("schedule other promise", .{});
const event_loop = vm.eventLoop();
// use a flag here instead of `this.all_closed_promise.get().isHandled(vm)` to prevent the race condition of this block being called
// again before the task has run.
this.flags.has_handled_all_closed_promise = true;
const task = ServerAllConnectionsClosedTask.new(.{
.globalObject = this.globalThis,
// Duplicate the Strong handle so that we can hold two independent strong references to it.
.promise = .{
.strong = .create(this.all_closed_promise.value(), this.globalThis),
},
.tracker = jsc.Debugger.AsyncTaskTracker.init(vm),
});
event_loop.enqueueTask(jsc.Task.init(task));
}
if (this.pending_requests == 0 and
this.listener == null and
!this.hasActiveWebSockets())
{
if (this.config.websocket) |*ws| {
ws.handler.app = null;
}
this.unref();
// Detach DevServer. This is needed because there are aggressive
// tests that check for DevServer memory soundness. This reveals
// a larger problem, that it seems that some objects like Server
// should be detachable from their JSValue, so that when the
// native handle is done, keeping the JS binding doesn't use
// `this.memoryCost()` bytes.
if (this.dev_server) |dev| {
this.dev_server = null;
if (this.app) |app| app.clearRoutes();
dev.deinit();
}
// Only free the memory if the JS reference has been freed too
if (this.flags.has_js_deinited) {
this.scheduleDeinit();
}
}
}
pub fn stopListening(this: *ThisServer, abrupt: bool) void {
httplog("stopListening", .{});
var listener = this.listener orelse return;
this.listener = null;
this.unref();
if (!ssl_enabled)
this.vm.removeListeningSocketForWatchMode(listener.socket().fd());
this.notifyInspectorServerStopped();
if (!abrupt) {
listener.close();
} else if (!this.flags.terminated) {
if (this.config.websocket) |*ws| {
ws.handler.app = null;
}
this.flags.terminated = true;
this.app.?.close();
}
}
pub fn stop(this: *ThisServer, abrupt: bool) void {
this.js_value.deinit();
if (this.config.allow_hot and this.config.id.len > 0) {
if (this.globalThis.bunVM().hotMap()) |hot| {
hot.remove(this.config.id);
}
}
this.stopListening(abrupt);
this.deinitIfWeCan();
}
pub fn scheduleDeinit(this: *ThisServer) void {
if (this.flags.deinit_scheduled) {
httplog("scheduleDeinit (again)", .{});
return;
}
this.flags.deinit_scheduled = true;
httplog("scheduleDeinit", .{});
if (!this.flags.terminated) {
// App.close can cause finalizers to run.
// scheduleDeinit can be called inside a finalizer.
// Therefore, we split it into two tasks.
this.flags.terminated = true;
const task = bun.default_allocator.create(jsc.AnyTask) catch unreachable;
task.* = jsc.AnyTask.New(App, App.close).init(this.app.?);
this.vm.enqueueTask(jsc.Task.init(task));
}
const task = bun.default_allocator.create(jsc.AnyTask) catch unreachable;
task.* = jsc.AnyTask.New(ThisServer, deinit).init(this);
this.vm.enqueueTask(jsc.Task.init(task));
}
fn notifyInspectorServerStopped(this: *ThisServer) void {
if (this.inspector_server_id.toOptional().unwrap() != null) {
@branchHint(.unlikely);
if (this.vm.debugger) |*debugger| {
@branchHint(.unlikely);
debugger.http_server_agent.notifyServerStopped(
AnyServer.from(this),
);
this.inspector_server_id = .init(0);
}
}
}
pub fn deinit(this: *ThisServer) void {
httplog("deinit", .{});
// This should've already been handled in stopListening
// However, when the JS VM terminates, it hypothetically might not call stopListening
this.notifyInspectorServerStopped();
this.cached_hostname.deref();
this.all_closed_promise.deinit();
for (this.user_routes.items) |*user_route| {
user_route.deinit();
}
this.user_routes.deinit(bun.default_allocator);
this.config.deinit();
this.on_clienterror.deinit();
if (this.app) |app| {
this.app = null;
app.destroy();
}
if (this.dev_server) |dev_server| {
dev_server.deinit();
}
if (this.plugins) |plugins| {
plugins.deref();
}
bun.destroy(this);
}
pub fn init(config: *ServerConfig, global: *JSGlobalObject) bun.JSOOM!*ThisServer {
const base_url = try bun.default_allocator.dupe(u8, strings.trim(config.base_url.href, "/"));
errdefer bun.default_allocator.free(base_url);
const dev_server = if (config.bake) |*bake_options|
try bun.bake.DevServer.init(.{
.arena = bake_options.arena.allocator(),
.root = bake_options.root,
.framework = bake_options.framework,
.bundler_options = bake_options.bundler_options,
.vm = global.bunVM(),
.broadcast_console_log_from_browser_to_server = config.broadcast_console_log_from_browser_to_server_for_bake,
})
else
null;
errdefer if (dev_server) |d| d.deinit();
var server = ThisServer.new(.{
.globalThis = global,
.config = config.*,
.base_url_string_for_joining = base_url,
.vm = jsc.VirtualMachine.get(),
.allocator = Arena.getThreadlocalDefault(),
.dev_server = dev_server,
});
if (RequestContext.pool == null) {
RequestContext.pool = bun.create(
server.allocator,
RequestContext.RequestContextStackAllocator,
RequestContext.RequestContextStackAllocator.init(bun.typedAllocator(RequestContext)),
);
}
server.request_pool_allocator = RequestContext.pool.?;
if (comptime ssl_enabled) {
analytics.Features.https_server += 1;
} else {
analytics.Features.http_server += 1;
}
return server;
}
noinline fn onListenFailed(this: *ThisServer) void {
httplog("onListenFailed", .{});
const globalThis = this.globalThis;
var error_instance = jsc.JSValue.zero;
var output_buf: [4096]u8 = undefined;
if (comptime ssl_enabled) {
output_buf[0] = 0;
var written: usize = 0;
var ssl_error = BoringSSL.ERR_get_error();
while (ssl_error != 0 and written < output_buf.len) : (ssl_error = BoringSSL.ERR_get_error()) {
if (written > 0) {
output_buf[written] = '\n';
written += 1;
}
if (BoringSSL.ERR_reason_error_string(
ssl_error,
)) |reason_ptr| {
const reason = std.mem.span(reason_ptr);
if (reason.len == 0) {
break;
}
@memcpy(output_buf[written..][0..reason.len], reason);
written += reason.len;
}
if (BoringSSL.ERR_func_error_string(
ssl_error,
)) |reason_ptr| {
const reason = std.mem.span(reason_ptr);
if (reason.len > 0) {
output_buf[written..][0.." via ".len].* = " via ".*;
written += " via ".len;
@memcpy(output_buf[written..][0..reason.len], reason);
written += reason.len;
}
}
if (BoringSSL.ERR_lib_error_string(
ssl_error,
)) |reason_ptr| {
const reason = std.mem.span(reason_ptr);
if (reason.len > 0) {
output_buf[written..][0] = ' ';
written += 1;
@memcpy(output_buf[written..][0..reason.len], reason);
written += reason.len;
}
}
}
if (written > 0) {
const message = output_buf[0..written];
error_instance = globalThis.createErrorInstance("OpenSSL {s}", .{message});
BoringSSL.ERR_clear_error();
}
}
if (error_instance == .zero) {
switch (this.config.address) {
.tcp => |tcp| {
error_set: {
if (comptime Environment.isLinux) {
const rc: i32 = -1;
const code = Sys.getErrno(rc);
if (code == bun.sys.E.ACCES) {
error_instance = (jsc.SystemError{
.message = bun.String.init(std.fmt.bufPrint(&output_buf, "permission denied {s}:{d}", .{ tcp.hostname orelse "0.0.0.0", tcp.port }) catch "Failed to start server"),
.code = bun.String.static("EACCES"),
.syscall = bun.String.static("listen"),
}).toErrorInstance(globalThis);
break :error_set;
}
}
error_instance = (jsc.SystemError{
.message = bun.String.init(std.fmt.bufPrint(&output_buf, "Failed to start server. Is port {d} in use?", .{tcp.port}) catch "Failed to start server"),
.code = bun.String.static("EADDRINUSE"),
.syscall = bun.String.static("listen"),
}).toErrorInstance(globalThis);
}
},
.unix => |unix| {
switch (bun.sys.getErrno(@as(i32, -1))) {
.SUCCESS => {
error_instance = (jsc.SystemError{
.message = bun.String.init(std.fmt.bufPrint(&output_buf, "Failed to listen on unix socket {}", .{bun.fmt.QuotedFormatter{ .text = unix }}) catch "Failed to start server"),
.code = bun.String.static("EADDRINUSE"),
.syscall = bun.String.static("listen"),
}).toErrorInstance(globalThis);
},
else => |e| {
var sys_err = bun.sys.Error.fromCode(e, .listen);
sys_err.path = unix;
error_instance = sys_err.toJS(globalThis);
},
}
},
}
}
error_instance.ensureStillAlive();
globalThis.throwValue(error_instance) catch {};
}
pub fn onListen(this: *ThisServer, socket: ?*App.ListenSocket) void {
if (socket == null) {
return this.onListenFailed();
}
this.listener = socket;
this.vm.event_loop_handle = Async.Loop.get();
if (!ssl_enabled)
this.vm.addListeningSocketForWatchMode(socket.?.socket().fd());
}
pub fn ref(this: *ThisServer) void {
if (this.poll_ref.isActive()) return;
this.poll_ref.ref(this.vm);
}
pub fn unref(this: *ThisServer) void {
if (!this.poll_ref.isActive()) return;
this.poll_ref.unref(this.vm);
}
pub fn doRef(this: *ThisServer, _: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const this_value = callframe.this();
this.ref();
return this_value;
}
pub fn doUnref(this: *ThisServer, _: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const this_value = callframe.this();
this.unref();
return this_value;
}
pub fn onBunInfoRequest(this: *ThisServer, req: *uws.Request, resp: *App.Response) void {
jsc.markBinding(@src());
this.pending_requests += 1;
defer this.pending_requests -= 1;
req.setYield(false);
var stack_fallback = std.heap.stackFallback(8192, this.allocator);
const allocator = stack_fallback.get();
const buffer_writer = js_printer.BufferWriter.init(allocator);
var writer = js_printer.BufferPrinter.init(buffer_writer);
defer writer.ctx.buffer.deinit();
const source = &logger.Source.initEmptyFile("info.json");
_ = js_printer.printJSON(
*js_printer.BufferPrinter,
&writer,
bun.Global.BunInfo.generate(*Transpiler, &jsc.VirtualMachine.get().transpiler, allocator) catch unreachable,
source,
.{ .mangled_props = null },
) catch unreachable;
resp.writeStatus("200 OK");
resp.writeHeader("Content-Type", MimeType.json.value);
resp.writeHeader("Cache-Control", "public, max-age=3600");
resp.writeHeaderInt("Age", 0);
const buffer = writer.ctx.written;
resp.end(buffer, false);
}
pub fn onPendingRequest(this: *ThisServer) void {
this.pending_requests += 1;
}
pub fn onNodeHTTPRequestWithUpgradeCtx(this: *ThisServer, req: *uws.Request, resp: *App.Response, upgrade_ctx: ?*uws.SocketContext) void {
this.onPendingRequest();
if (comptime Environment.isDebug) {
this.vm.eventLoop().debug.enter();
}
defer {
if (comptime Environment.isDebug) {
this.vm.eventLoop().debug.exit();
}
}
req.setYield(false);
resp.timeout(this.config.idleTimeout);
const globalThis = this.globalThis;
const thisObject: JSValue = this.js_value.get() orelse .js_undefined;
const vm = this.vm;
var node_http_response: ?*NodeHTTPResponse = null;
var is_async = false;
defer {
if (!is_async) {
if (node_http_response) |node_response| {
node_response.deref();
}
}
}
const result: JSValue = bun.jsc.fromJSHostCall(globalThis, @src(), onNodeHTTPRequestFn, .{
@intFromPtr(AnyServer.from(this).ptr.ptr()),
globalThis,
thisObject,
this.config.onNodeHTTPRequest,
if (bun.http.Method.find(req.method())) |method|
method.toJS(globalThis)
else
.js_undefined,
req,
resp,
upgrade_ctx,
&node_http_response,
}) catch globalThis.takeException(error.JSError);
const HTTPResult = union(enum) {
rejection: jsc.JSValue,
exception: jsc.JSValue,
success: void,
pending: jsc.JSValue,
};
var strong_promise: jsc.Strong.Optional = .empty;
var needs_to_drain = true;
defer {
if (needs_to_drain) {
vm.drainMicrotasks();
}
}
defer strong_promise.deinit();
const http_result: HTTPResult = brk: {
if (result.toError()) |err| {
break :brk .{ .exception = err };
}
if (result.asAnyPromise()) |promise| {
if (promise.status(globalThis.vm()) == .pending) {
strong_promise.set(globalThis, result);
needs_to_drain = false;
vm.drainMicrotasks();
}
switch (promise.status(globalThis.vm())) {
.fulfilled => {
globalThis.handleRejectedPromises();
break :brk .{ .success = {} };
},
.rejected => {
promise.setHandled(globalThis.vm());
break :brk .{ .rejection = promise.result(globalThis.vm()) };
},
.pending => {
globalThis.handleRejectedPromises();
if (node_http_response) |node_response| {
if (node_response.flags.request_has_completed or node_response.flags.socket_closed or node_response.flags.upgraded) {
strong_promise.deinit();
break :brk .{ .success = {} };
}
const strong_self = node_response.getThisValue();
if (strong_self.isEmptyOrUndefinedOrNull()) {
strong_promise.deinit();
break :brk .{ .success = {} };
}
node_response.promise = strong_promise;
strong_promise = .empty;
result._then2(globalThis, strong_self, NodeHTTPResponse.Bun__NodeHTTPRequest__onResolve, NodeHTTPResponse.Bun__NodeHTTPRequest__onReject);
is_async = true;
}
break :brk .{ .pending = result };
},
}
}
break :brk .{ .success = {} };
};
switch (http_result) {
.exception, .rejection => |err| {
_ = vm.uncaughtException(globalThis, err, http_result == .rejection);
if (node_http_response) |node_response| {
if (!node_response.flags.request_has_completed and node_response.raw_response.state().isResponsePending()) {
if (node_response.raw_response.state().isHttpStatusCalled()) {
node_response.raw_response.writeStatus("500 Internal Server Error");
node_response.raw_response.endWithoutBody(true);
} else {
node_response.raw_response.endStream(true);
}
}
node_response.onRequestComplete();
}
},
.success => {},
.pending => {},
}
if (node_http_response) |node_response| {
if (!node_response.flags.upgraded) {
if (!node_response.flags.request_has_completed and node_response.raw_response.state().isResponsePending()) {
node_response.setOnAbortedHandler();
}
// If we ended the response without attaching an ondata handler, we discard the body read stream
else if (http_result != .pending) {
node_response.maybeStopReadingBody(vm, node_response.getThisValue());
}
}
}
}
pub fn onNodeHTTPRequest(
this: *ThisServer,
req: *uws.Request,
resp: *App.Response,
) void {
jsc.markBinding(@src());
onNodeHTTPRequestWithUpgradeCtx(this, req, resp, null);
}
const onNodeHTTPRequestFn = if (ssl_enabled)
NodeHTTPServer__onRequest_https
else
NodeHTTPServer__onRequest_http;
pub fn setUsingCustomExpectHandler(this: *ThisServer, value: bool) void {
NodeHTTP_setUsingCustomExpectHandler(ssl_enabled, this.app.?, value);
}
var did_send_idletimeout_warning_once = false;
fn onTimeoutForIdleWarn(_: *anyopaque, _: *App.Response) void {
if (debug_mode and !did_send_idletimeout_warning_once) {
if (!bun.cli.Command.get().debug.silent) {
did_send_idletimeout_warning_once = true;
Output.prettyErrorln("<r><yellow>[Bun.serve]<r><d>:<r> request timed out after 10 seconds. Pass <d><cyan>`idleTimeout`<r> to configure.", .{});
Output.flush();
}
}
}
fn shouldAddTimeoutHandlerForWarning(server: *ThisServer) bool {
if (comptime debug_mode) {
if (!did_send_idletimeout_warning_once and !bun.cli.Command.get().debug.silent) {
return !server.config.has_idleTimeout;
}
}
return false;
}
pub fn onUserRouteRequest(user_route: *UserRoute, req: *uws.Request, resp: *App.Response) void {
const server = user_route.server;
const index = user_route.id;
var should_deinit_context = false;
var prepared = server.prepareJsRequestContext(req, resp, &should_deinit_context, false, switch (user_route.route.method) {
.any => null,
.specific => |m| m,
}) orelse return;
const server_request_list = js.routeListGetCached(server.jsValueAssertAlive()).?;
const response_value = bun.jsc.fromJSHostCall(server.globalThis, @src(), Bun__ServerRouteList__callRoute, .{ server.globalThis, index, prepared.request_object, server.jsValueAssertAlive(), server_request_list, &prepared.js_request, req }) catch |err| server.globalThis.takeException(err);
server.handleRequest(&should_deinit_context, prepared, req, response_value);
}
fn handleRequest(this: *ThisServer, should_deinit_context: *bool, prepared: PreparedRequest, req: *uws.Request, response_value: jsc.JSValue) void {
const ctx = prepared.ctx;
defer {
// uWS request will not live longer than this function
prepared.request_object.request_context.detachRequest();
}
ctx.onResponse(this, prepared.js_request, response_value);
// Reference in the stack here in case it is not for whatever reason
prepared.js_request.ensureStillAlive();
ctx.defer_deinit_until_callback_completes = null;
if (should_deinit_context.*) {
ctx.deinit();
return;
}
if (ctx.shouldRenderMissing()) {
ctx.renderMissing();
return;
}
// The request is asynchronous, and all information from `req` must be copied
// since the provided uws.Request will be re-used for future requests (stack allocated).
ctx.toAsync(req, prepared.request_object);
}
pub fn onRequest(
this: *ThisServer,
req: *uws.Request,
resp: *App.Response,
) void {
var should_deinit_context = false;
const prepared = this.prepareJsRequestContext(req, resp, &should_deinit_context, true, null) orelse return;
bun.assert(this.config.onRequest != .zero);
const js_value = this.jsValueAssertAlive();
const response_value = this.config.onRequest.call(
this.globalThis,
js_value,
&.{ prepared.js_request, js_value },
) catch |err|
this.globalThis.takeException(err);
this.handleRequest(&should_deinit_context, prepared, req, response_value);
}
pub fn onRequestFromSaved(
this: *ThisServer,
req: SavedRequest.Union,
resp: *App.Response,
callback: JSValue,
comptime arg_count: comptime_int,
extra_args: [arg_count]JSValue,
) void {
const prepared: PreparedRequest = switch (req) {
.stack => |r| this.prepareJsRequestContext(r, resp, null, true, null) orelse return,
.saved => |data| .{
.js_request = data.js_request.get() orelse @panic("Request was unexpectedly freed"),
.request_object = data.request,
.ctx = data.ctx.tagged_pointer.as(RequestContext),
},
};
const ctx = prepared.ctx;
bun.assert(callback != .zero);
const args = .{prepared.js_request} ++ extra_args;
const response_value = callback.call(
this.globalThis,
this.jsValueAssertAlive(),
&args,
) catch |err|
this.globalThis.takeException(err);
defer if (req == .stack) {
// uWS request will not live longer than this function
prepared.request_object.request_context.detachRequest();
};
const original_state = ctx.defer_deinit_until_callback_completes;
var should_deinit_context = false;
ctx.defer_deinit_until_callback_completes = &should_deinit_context;
ctx.onResponse(this, prepared.js_request, response_value);
ctx.defer_deinit_until_callback_completes = original_state;
// Reference in the stack here in case it is not for whatever reason
prepared.js_request.ensureStillAlive();
if (should_deinit_context) {
ctx.deinit();
return;
}
if (ctx.shouldRenderMissing()) {
ctx.renderMissing();
return;
}
// The request is asynchronous, and all information from `req` must be copied
// since the provided uws.Request will be re-used for future requests (stack allocated).
switch (req) {
.stack => |r| ctx.toAsync(r, prepared.request_object),
.saved => {}, // info already copied
}
}
pub const PreparedRequest = struct {
js_request: JSValue,
request_object: *Request,
ctx: *RequestContext,
/// This is used by DevServer for deferring calling the JS handler
/// to until the bundle is actually ready.
pub fn save(
prepared: PreparedRequest,
global: *jsc.JSGlobalObject,
req: *uws.Request,
resp: *App.Response,
) SavedRequest {
// By saving a request, all information from `req` must be
// copied since the provided uws.Request will be re-used for
// future requests (stack allocated).
prepared.ctx.toAsync(req, prepared.request_object);
return .{
.js_request = .create(prepared.js_request, global),
.request = prepared.request_object,
.ctx = AnyRequestContext.init(prepared.ctx),
.response = uws.AnyResponse.init(resp),
};
}
};
pub fn prepareJsRequestContext(this: *ThisServer, req: *uws.Request, resp: *App.Response, should_deinit_context: ?*bool, create_js_request: bool, method: ?bun.http.Method) ?PreparedRequest {
jsc.markBinding(@src());
this.onPendingRequest();
if (comptime Environment.isDebug) {
this.vm.eventLoop().debug.enter();
}
defer {
if (comptime Environment.isDebug) {
this.vm.eventLoop().debug.exit();
}
}
req.setYield(false);
resp.timeout(this.config.idleTimeout);
// Since we do timeouts by default, we should tell the user when
// this happens - but limit it to only warn once.
if (shouldAddTimeoutHandlerForWarning(this)) {
// We need to pass it a pointer, any pointer should do.
resp.onTimeout(*anyopaque, onTimeoutForIdleWarn, &did_send_idletimeout_warning_once);
}
const ctx = this.request_pool_allocator.tryGet() catch bun.outOfMemory();
ctx.create(this, req, resp, should_deinit_context, method);
this.vm.jsc_vm.reportExtraMemory(@sizeOf(RequestContext));
const body = this.vm.initRequestBodyValue(.{ .Null = {} }) catch unreachable;
ctx.request_body = body;
var signal = jsc.WebCore.AbortSignal.new(this.globalThis);
ctx.signal = signal;
signal.pendingActivityRef();
const request_object = Request.new(.{
.method = ctx.method,
.request_context = AnyRequestContext.init(ctx),
.https = ssl_enabled,
.signal = signal.ref(),
.body = body.ref(),
});
ctx.request_weakref = .initRef(request_object);
if (comptime debug_mode) {
ctx.flags.is_web_browser_navigation = brk: {
if (req.header("sec-fetch-dest")) |fetch_dest| {
if (strings.eqlComptime(fetch_dest, "document")) {
break :brk true;
}
}
break :brk false;
};
}
// we need to do this very early unfortunately
// it seems to work fine for synchronous requests but anything async will take too long to register the handler
// we do this only for HTTP methods that support request bodies, so not GET, HEAD, OPTIONS, or CONNECT.
if ((HTTP.Method.which(req.method()) orelse HTTP.Method.OPTIONS).hasRequestBody()) {
const req_len: usize = brk: {
if (req.header("content-length")) |content_length| {
break :brk std.fmt.parseInt(usize, content_length, 10) catch 0;
}
break :brk 0;
};
if (req_len > this.config.max_request_body_size) {
resp.writeStatus("413 Request Entity Too Large");
resp.endWithoutBody(true);
this.finalize();
return null;
}
ctx.request_body_content_len = req_len;
ctx.flags.is_transfer_encoding = req.header("transfer-encoding") != null;
if (req_len > 0 or ctx.flags.is_transfer_encoding) {
// we defer pre-allocating the body until we receive the first chunk
// that way if the client is lying about how big the body is or the client aborts
// we don't waste memory
ctx.request_body.?.value = .{
.Locked = .{
.task = ctx,
.global = this.globalThis,
.onStartBuffering = RequestContext.onStartBufferingCallback,
.onStartStreaming = RequestContext.onStartStreamingRequestBodyCallback,
.onReadableStreamAvailable = RequestContext.onRequestBodyReadableStreamAvailable,
},
};
ctx.flags.is_waiting_for_request_body = true;
resp.onData(*RequestContext, RequestContext.onBufferedBodyChunk, ctx);
}
}
return .{
.js_request = if (create_js_request) request_object.toJS(this.globalThis) else .zero,
.request_object = request_object,
.ctx = ctx,
};
}
fn upgradeWebSocketUserRoute(this: *UserRoute, resp: *App.Response, req: *uws.Request, upgrade_ctx: *uws.SocketContext, method: ?bun.http.Method) void {
const server = this.server;
const index = this.id;
var should_deinit_context = false;
var prepared = server.prepareJsRequestContext(req, resp, &should_deinit_context, false, method) orelse return;
prepared.ctx.upgrade_context = upgrade_ctx; // set the upgrade context
const server_request_list = js.routeListGetCached(server.jsValueAssertAlive()).?;
const response_value = bun.jsc.fromJSHostCall(server.globalThis, @src(), Bun__ServerRouteList__callRoute, .{ server.globalThis, index, prepared.request_object, server.jsValueAssertAlive(), server_request_list, &prepared.js_request, req }) catch |err| server.globalThis.takeException(err);
server.handleRequest(&should_deinit_context, prepared, req, response_value);
}
pub fn onWebSocketUpgrade(
this: *ThisServer,
resp: *App.Response,
req: *uws.Request,
upgrade_ctx: *uws.SocketContext,
id: usize,
) void {
jsc.markBinding(@src());
if (id == 1) {
// This is actually a UserRoute if id is 1 so it's safe to cast
upgradeWebSocketUserRoute(@ptrCast(this), resp, req, upgrade_ctx, null);
return;
}
// Access `this` as *ThisServer only if id is 0
bun.assert(id == 0);
if (this.config.onNodeHTTPRequest != .zero) {
onNodeHTTPRequestWithUpgradeCtx(this, req, resp, upgrade_ctx);
return;
}
if (this.config.onRequest == .zero) {
// require fetch method to be set otherwise we dont know what route to call
// this should be the fallback in case no route is provided to upgrade
resp.writeStatus("403 Forbidden");
resp.endWithoutBody(true);
return;
}
this.pending_requests += 1;
req.setYield(false);
var ctx = this.request_pool_allocator.tryGet() catch bun.outOfMemory();
var should_deinit_context = false;
ctx.create(this, req, resp, &should_deinit_context, null);
var body = this.vm.initRequestBodyValue(.{ .Null = {} }) catch unreachable;
ctx.request_body = body;
var signal = jsc.WebCore.AbortSignal.new(this.globalThis);
ctx.signal = signal;
var request_object = Request.new(.{
.method = ctx.method,
.request_context = AnyRequestContext.init(ctx),
.https = ssl_enabled,
.signal = signal.ref(),
.body = body.ref(),
});
ctx.upgrade_context = upgrade_ctx;
ctx.request_weakref = .initRef(request_object);
// We keep the Request object alive for the duration of the request so that we can remove the pointer to the UWS request object.
var args = [_]jsc.JSValue{
request_object.toJS(this.globalThis),
this.jsValueAssertAlive(),
};
const request_value = args[0];
request_value.ensureStillAlive();
const response_value = this.config.onRequest.call(this.globalThis, this.jsValueAssertAlive(), &args) catch |err|
this.globalThis.takeException(err);
defer {
// uWS request will not live longer than this function
request_object.request_context.detachRequest();
}
ctx.onResponse(
this,
request_value,
response_value,
);
ctx.defer_deinit_until_callback_completes = null;
if (should_deinit_context) {
ctx.deinit();
return;
}
if (ctx.shouldRenderMissing()) {
ctx.renderMissing();
return;
}
ctx.toAsync(req, request_object);
}
// https://chromium.googlesource.com/devtools/devtools-frontend/+/main/docs/ecosystem/automatic_workspace_folders.md
fn onChromeDevToolsJSONRequest(this: *ThisServer, req: *uws.Request, resp: *App.Response) void {
if (comptime Environment.enable_logs)
httplog("{s} - {s}", .{ req.method(), req.url() });
const authorized = brk: {
if (this.dev_server == null)
break :brk false;
if (resp.getRemoteSocketInfo()) |*address| {
// IPv4 loopback addresses
if (strings.startsWith(address.ip, "127.")) {
break :brk true;
}
// IPv6 loopback addresses
if (strings.startsWith(address.ip, "::ffff:127.") or
strings.startsWith(address.ip, "::1") or
strings.eqlComptime(address.ip, "0:0:0:0:0:0:0:1"))
{
break :brk true;
}
}
break :brk false;
};
if (!authorized) {
req.setYield(true);
return;
}
// They need a 16 byte uuid. It needs to be somewhat consistent. We don't want to store this field anywhere.
// So we first use a hash of the main field:
const first_hash_segment: [8]u8 = brk: {
const buffer = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(buffer);
const main = jsc.VirtualMachine.get().main;
const len = @min(main.len, buffer.len);
break :brk @bitCast(bun.hash(bun.strings.copyLowercase(main[0..len], buffer[0..len])));
};
// And then we use a hash of their project root directory:
const second_hash_segment: [8]u8 = brk: {
const buffer = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(buffer);
const root = this.dev_server.?.root;
const len = @min(root.len, buffer.len);
break :brk @bitCast(bun.hash(bun.strings.copyLowercase(root[0..len], buffer[0..len])));
};
// We combine it together to get a 16 byte uuid.
const hash_bytes: [16]u8 = first_hash_segment ++ second_hash_segment;
const uuid = bun.UUID.initWith(&hash_bytes);
// interface DevToolsJSON {
// workspace?: {
// root: string,
// uuid: string,
// }
// }
const json_string = std.fmt.allocPrint(bun.default_allocator, "{{ \"workspace\": {{ \"root\": {}, \"uuid\": \"{}\" }} }}", .{
bun.fmt.formatJSONStringUTF8(this.dev_server.?.root, .{}),
uuid,
}) catch bun.outOfMemory();
defer bun.default_allocator.free(json_string);
resp.writeStatus("200 OK");
resp.writeHeader("Content-Type", "application/json");
resp.end(json_string, resp.shouldCloseConnection());
}
fn setRoutes(this: *ThisServer) jsc.JSValue {
var route_list_value = jsc.JSValue.zero;
const app = this.app.?;
const any_server = AnyServer.from(this);
const dev_server = this.dev_server;
// https://chromium.googlesource.com/devtools/devtools-frontend/+/main/docs/ecosystem/automatic_workspace_folders.md
// Only enable this when we're using the dev server.
var should_add_chrome_devtools_json_route = debug_mode and this.config.allow_hot and dev_server != null and this.config.enable_chrome_devtools_automatic_workspace_folders;
const chrome_devtools_route = "/.well-known/appspecific/com.chrome.devtools.json";
// --- 1. Handle user_routes_to_build (dynamic JS routes) ---
// (This part remains conceptually the same: populate this.user_routes and route_list_value
// Crucially, ServerConfig.fromJS must ensure `route.method` is correctly .specific or .any)
if (this.config.user_routes_to_build.items.len > 0) {
var user_routes_to_build_list = this.config.user_routes_to_build.moveToUnmanaged();
var old_user_routes = this.user_routes;
defer {
for (old_user_routes.items) |*r| r.route.deinit();
old_user_routes.deinit(bun.default_allocator);
}
this.user_routes = std.ArrayListUnmanaged(UserRoute).initCapacity(bun.default_allocator, user_routes_to_build_list.items.len) catch @panic("OOM");
const paths_zig = bun.default_allocator.alloc(ZigString, user_routes_to_build_list.items.len) catch @panic("OOM");
defer bun.default_allocator.free(paths_zig);
const callbacks_js = bun.default_allocator.alloc(jsc.JSValue, user_routes_to_build_list.items.len) catch @panic("OOM");
defer bun.default_allocator.free(callbacks_js);
for (user_routes_to_build_list.items, paths_zig, callbacks_js, 0..) |*builder, *p_zig, *cb_js, i| {
p_zig.* = ZigString.init(builder.route.path);
cb_js.* = builder.callback.get().?;
this.user_routes.appendAssumeCapacity(.{
.id = @truncate(i),
.server = this,
.route = builder.route,
});
builder.route = .{}; // Mark as moved
}
route_list_value = Bun__ServerRouteList__create(this.globalThis, callbacks_js.ptr, paths_zig.ptr, user_routes_to_build_list.items.len);
for (user_routes_to_build_list.items) |*builder| builder.deinit();
user_routes_to_build_list.deinit(bun.default_allocator);
}
// --- 2. Setup WebSocket handler's app reference ---
if (this.config.websocket) |*websocket| {
websocket.globalObject = this.globalThis;
websocket.handler.app = app;
websocket.handler.flags.ssl = ssl_enabled;
}
// --- 3. Register compiled user routes (this.user_routes) & Track "/*" Coverage ---
var star_methods_covered_by_user = bun.http.Method.Set.initEmpty();
var has_any_user_route_for_star_path = false; // True if "/*" path appears in user_routes at all
var has_any_ws_route_for_star_path = false;
for (this.user_routes.items) |*user_route| {
const is_star_path = strings.eqlComptime(user_route.route.path, "/*");
if (is_star_path) {
has_any_user_route_for_star_path = true;
}
if (should_add_chrome_devtools_json_route) {
if (strings.eqlComptime(user_route.route.path, chrome_devtools_route) or strings.hasPrefix(user_route.route.path, "/.well-known/")) {
should_add_chrome_devtools_json_route = false;
}
}
// Register HTTP routes
switch (user_route.route.method) {
.any => {
app.any(user_route.route.path, *UserRoute, user_route, onUserRouteRequest);
if (is_star_path) {
star_methods_covered_by_user = .initFull();
}
if (this.config.websocket) |*websocket| {
if (is_star_path) {
has_any_ws_route_for_star_path = true;
}
app.ws(
user_route.route.path,
user_route,
1, // id 1 means is a user route
ServerWebSocket.behavior(ThisServer, ssl_enabled, websocket.toBehavior()),
);
}
},
.specific => |method_val| { // method_val is HTTP.Method here
app.method(method_val, user_route.route.path, *UserRoute, user_route, onUserRouteRequest);
if (is_star_path) {
star_methods_covered_by_user.insert(method_val);
}
// Setup user websocket in the route if needed.
if (this.config.websocket) |*websocket| {
// Websocket upgrade is a GET request
if (method_val == .GET) {
app.ws(
user_route.route.path,
user_route,
1, // id 1 means is a user route
ServerWebSocket.behavior(ThisServer, ssl_enabled, websocket.toBehavior()),
);
}
}
},
}
}
// --- 4. Register negative routes ---
for (this.config.negative_routes.items) |route_path| {
app.head(route_path, *ThisServer, this, onRequest);
app.any(route_path, *ThisServer, this, onRequest);
}
// --- 5. Register static routes & Track "/*" Coverage ---
var needs_plugins = dev_server != null;
var has_static_route_for_star_path = false;
if (this.config.static_routes.items.len > 0) {
for (this.config.static_routes.items) |*entry| {
if (strings.eqlComptime(entry.path, "/*")) {
has_static_route_for_star_path = true;
switch (entry.method) {
.any => {
star_methods_covered_by_user = .initFull();
},
.method => |method| {
star_methods_covered_by_user.setUnion(method);
},
}
}
if (should_add_chrome_devtools_json_route) {
if (strings.eqlComptime(entry.path, chrome_devtools_route) or strings.hasPrefix(entry.path, "/.well-known/")) {
should_add_chrome_devtools_json_route = false;
}
}
switch (entry.route) {
.static => |static_route| {
ServerConfig.applyStaticRoute(any_server, ssl_enabled, app, *StaticRoute, static_route, entry.path, entry.method);
},
.file => |file_route| {
ServerConfig.applyStaticRoute(any_server, ssl_enabled, app, *FileRoute, file_route, entry.path, entry.method);
},
.html => |html_bundle_route| {
ServerConfig.applyStaticRoute(any_server, ssl_enabled, app, *HTMLBundle.Route, html_bundle_route.data, entry.path, entry.method);
if (dev_server) |dev| {
dev.html_router.put(dev.allocator, entry.path, html_bundle_route.data) catch bun.outOfMemory();
}
needs_plugins = true;
},
.framework_router => {},
}
}
}
// --- 6. Initialize plugins if needed ---
if (needs_plugins and this.plugins == null) {
if (this.vm.transpiler.options.serve_plugins) |serve_plugins_config| {
if (serve_plugins_config.len > 0) {
this.plugins = ServePlugins.init(serve_plugins_config);
}
}
}
// --- 7. Debug mode specific routes ---
if (debug_mode) {
app.get("/bun:info", *ThisServer, this, onBunInfoRequest);
if (this.config.inspector) {
jsc.markBinding(@src());
Bun__addInspector(ssl_enabled, app, this.globalThis);
}
}
// --- 8. Handle DevServer routes & Track "/*" Coverage ---
var has_dev_server_for_star_path = false;
if (dev_server) |dev| {
// dev.setRoutes might register its own "/*" HTTP handler
has_dev_server_for_star_path = dev.setRoutes(this) catch bun.outOfMemory();
if (has_dev_server_for_star_path) {
// Assume dev server "/*" covers all methods if it exists
star_methods_covered_by_user = .initFull();
}
}
// Setup user websocket fallback route aka fetch function if fetch is not provided will respond with 403.
if (!has_any_ws_route_for_star_path) {
if (this.config.websocket) |*websocket| {
app.ws(
"/*",
this,
0, // id 0 means is a fallback route and ctx is the server
ServerWebSocket.behavior(ThisServer, ssl_enabled, websocket.toBehavior()),
);
}
}
// --- 9. Consolidated "/*" HTTP Fallback Registration ---
if (star_methods_covered_by_user.eql(bun.http.Method.Set.initFull())) {
// User/Static/Dev has already provided a "/*" handler for ALL methods.
// No further global "/*" HTTP fallback needed.
} else if (has_any_user_route_for_star_path or has_static_route_for_star_path or has_dev_server_for_star_path) {
// A "/*" route exists, but doesn't cover all methods.
// Apply the global handler to the *remaining* methods for "/*".
// So we flip the bits for the methods that are not covered by the user/static/dev routes
star_methods_covered_by_user.toggleAll();
var iter = star_methods_covered_by_user.iterator();
while (iter.next()) |method_to_cover| {
switch (this.config.onNodeHTTPRequest) {
.zero => switch (this.config.onRequest) {
.zero => app.method(method_to_cover, "/*", *ThisServer, this, on404),
else => app.method(method_to_cover, "/*", *ThisServer, this, onRequest),
},
else => app.method(method_to_cover, "/*", *ThisServer, this, onNodeHTTPRequest),
}
}
} else {
switch (this.config.onNodeHTTPRequest) {
.zero => switch (this.config.onRequest) {
.zero => app.any("/*", *ThisServer, this, on404),
else => app.any("/*", *ThisServer, this, onRequest),
},
else => app.any("/*", *ThisServer, this, onNodeHTTPRequest),
}
}
if (should_add_chrome_devtools_json_route) {
app.get(chrome_devtools_route, *ThisServer, this, onChromeDevToolsJSONRequest);
}
// If onNodeHTTPRequest is configured, it might be needed for Node.js compatibility layer
// for specific Node API routes, even if it's not the main "/*" handler.
if (this.config.onNodeHTTPRequest != .zero) {
NodeHTTP_assignOnCloseFunction(ssl_enabled, app);
}
return route_list_value;
}
pub fn on404(_: *ThisServer, req: *uws.Request, resp: *App.Response) void {
if (comptime Environment.enable_logs)
httplog("{s} - {s} 404", .{ req.method(), req.url() });
resp.writeStatus("404 Not Found");
// Rely on browser default page for now.
resp.end("", false);
}
// TODO: make this return JSError!void, and do not deinitialize on synchronous failure, to allow errdefer in caller scope
pub fn listen(this: *ThisServer) jsc.JSValue {
httplog("listen", .{});
var app: *App = undefined;
const globalThis = this.globalThis;
var route_list_value = jsc.JSValue.zero;
if (ssl_enabled) {
bun.BoringSSL.load();
const ssl_config = this.config.ssl_config orelse @panic("Assertion failure: ssl_config");
const ssl_options = ssl_config.asUSockets();
app = App.create(ssl_options) orelse {
if (!globalThis.hasException()) {
if (!throwSSLErrorIfNecessary(globalThis)) {
globalThis.throw("Failed to create HTTP server", .{}) catch {};
}
}
this.app = null;
this.deinit();
return .zero;
};
this.app = app;
route_list_value = this.setRoutes();
// add serverName to the SSL context using default ssl options
if (ssl_config.server_name) |server_name_ptr| {
const server_name: [:0]const u8 = std.mem.span(server_name_ptr);
if (server_name.len > 0) {
app.addServerNameWithOptions(server_name, ssl_options) catch {
if (!globalThis.hasException()) {
if (!throwSSLErrorIfNecessary(globalThis)) {
globalThis.throw("Failed to add serverName: {s}", .{server_name}) catch {};
}
}
this.deinit();
return .zero;
};
if (throwSSLErrorIfNecessary(globalThis)) {
this.deinit();
return .zero;
}
app.domain(server_name);
if (throwSSLErrorIfNecessary(globalThis)) {
this.deinit();
return .zero;
}
// Ensure the routes are set for that domain name.
_ = this.setRoutes();
}
}
// apply SNI routes if any
if (this.config.sni) |*sni| {
for (sni.slice()) |*sni_ssl_config| {
const sni_servername: [:0]const u8 = std.mem.span(sni_ssl_config.server_name);
if (sni_servername.len > 0) {
app.addServerNameWithOptions(sni_servername, sni_ssl_config.asUSockets()) catch {
if (!globalThis.hasException()) {
if (!throwSSLErrorIfNecessary(globalThis)) {
globalThis.throw("Failed to add serverName: {s}", .{sni_servername}) catch {};
}
}
this.deinit();
return .zero;
};
app.domain(sni_servername);
if (throwSSLErrorIfNecessary(globalThis)) {
this.deinit();
return .zero;
}
// Ensure the routes are set for that domain name.
_ = this.setRoutes();
}
}
}
} else {
app = App.create(.{}) orelse {
if (!globalThis.hasException()) {
globalThis.throw("Failed to create HTTP server", .{}) catch {};
}
this.deinit();
return .zero;
};
this.app = app;
route_list_value = this.setRoutes();
}
if (this.config.onNodeHTTPRequest != .zero) {
this.setUsingCustomExpectHandler(true);
}
switch (this.config.address) {
.tcp => |tcp| {
var host: ?[*:0]const u8 = null;
var host_buff: [1024:0]u8 = undefined;
if (tcp.hostname) |existing| {
const hostname = bun.span(existing);
if (hostname.len > 2 and hostname[0] == '[') {
// remove "[" and "]" from hostname
host = std.fmt.bufPrintZ(&host_buff, "{s}", .{hostname[1 .. hostname.len - 1]}) catch unreachable;
} else {
host = tcp.hostname;
}
}
app.listenWithConfig(*ThisServer, this, onListen, .{
.port = tcp.port,
.host = host,
.options = this.config.getUsocketsOptions(),
});
},
.unix => |unix| {
app.listenOnUnixSocket(
*ThisServer,
this,
onListen,
unix,
this.config.getUsocketsOptions(),
);
},
}
if (globalThis.hasException()) {
this.deinit();
return .zero;
}
this.ref();
// Starting up an HTTP server is a good time to GC
if (this.vm.aggressive_garbage_collection == .aggressive) {
this.vm.autoGarbageCollect();
} else {
this.vm.eventLoop().performGC();
}
return route_list_value;
}
pub fn onClientErrorCallback(this: *ThisServer, socket: *uws.Socket, error_code: u8, raw_packet: []const u8) void {
if (this.on_clienterror.get()) |callback| {
const is_ssl = protocol_enum == .https;
const node_socket = bun.jsc.fromJSHostCall(this.globalThis, @src(), Bun__createNodeHTTPServerSocket, .{ is_ssl, socket, this.globalThis }) catch return;
if (node_socket.isUndefinedOrNull()) return;
const error_code_value = JSValue.jsNumber(error_code);
const raw_packet_value = jsc.ArrayBuffer.createBuffer(this.globalThis, raw_packet) catch return; // TODO: properly propagate exception upwards
const loop = this.globalThis.bunVM().eventLoop();
loop.enter();
defer loop.exit();
_ = callback.call(this.globalThis, .js_undefined, &.{ JSValue.jsBoolean(is_ssl), node_socket, error_code_value, raw_packet_value }) catch |err| {
this.globalThis.reportActiveExceptionAsUnhandled(err);
};
}
}
};
}
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,
ctx: AnyRequestContext,
response: uws.AnyResponse,
pub fn deinit(sr: *SavedRequest) void {
sr.js_request.deinit();
sr.ctx.deref();
}
pub const Union = union(enum) {
stack: *uws.Request,
saved: bun.jsc.API.SavedRequest,
};
};
pub const ServerAllConnectionsClosedTask = struct {
globalObject: *jsc.JSGlobalObject,
promise: jsc.JSPromise.Strong,
tracker: jsc.Debugger.AsyncTaskTracker,
pub const new = bun.TrivialNew(@This());
pub fn runFromJSThread(this: *ServerAllConnectionsClosedTask, vm: *jsc.VirtualMachine) void {
httplog("ServerAllConnectionsClosedTask runFromJSThread", .{});
const globalObject = this.globalObject;
const tracker = this.tracker;
tracker.willDispatch(globalObject);
defer tracker.didDispatch(globalObject);
var promise = this.promise;
defer promise.deinit();
bun.destroy(this);
if (!vm.isShuttingDown()) {
promise.resolve(globalObject, .js_undefined);
}
}
};
pub const HTTPServer = NewServer(.http, .production);
pub const HTTPSServer = NewServer(.https, .production);
pub const DebugHTTPServer = NewServer(.http, .debug);
pub const DebugHTTPSServer = NewServer(.https, .debug);
pub const AnyServer = struct {
ptr: Ptr,
pub const Ptr = bun.TaggedPointerUnion(.{
HTTPServer,
HTTPSServer,
DebugHTTPServer,
DebugHTTPSServer,
});
pub const AnyUserRouteList = union(enum) {
HTTPServer: []const HTTPServer.UserRoute,
HTTPSServer: []const HTTPSServer.UserRoute,
DebugHTTPServer: []const DebugHTTPServer.UserRoute,
DebugHTTPSServer: []const DebugHTTPSServer.UserRoute,
};
pub fn userRoutes(this: AnyServer) AnyUserRouteList {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => .{ .HTTPServer = this.ptr.as(HTTPServer).user_routes.items },
Ptr.case(HTTPSServer) => .{ .HTTPSServer = this.ptr.as(HTTPSServer).user_routes.items },
Ptr.case(DebugHTTPServer) => .{ .DebugHTTPServer = this.ptr.as(DebugHTTPServer).user_routes.items },
Ptr.case(DebugHTTPSServer) => .{ .DebugHTTPSServer = this.ptr.as(DebugHTTPSServer).user_routes.items },
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn getURLAsString(this: AnyServer) bun.OOM!bun.String {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).getURLAsString(),
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).getURLAsString(),
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).getURLAsString(),
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).getURLAsString(),
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn vm(this: AnyServer) *jsc.VirtualMachine {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).vm,
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).vm,
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).vm,
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).vm,
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn setInspectorServerID(this: AnyServer, id: jsc.Debugger.DebuggerId) void {
switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => {
this.ptr.as(HTTPServer).inspector_server_id = id;
if (this.ptr.as(HTTPServer).dev_server) |dev_server| {
dev_server.inspector_server_id = id;
}
},
Ptr.case(HTTPSServer) => {
this.ptr.as(HTTPSServer).inspector_server_id = id;
if (this.ptr.as(HTTPSServer).dev_server) |dev_server| {
dev_server.inspector_server_id = id;
}
},
Ptr.case(DebugHTTPServer) => {
this.ptr.as(DebugHTTPServer).inspector_server_id = id;
if (this.ptr.as(DebugHTTPServer).dev_server) |dev_server| {
dev_server.inspector_server_id = id;
}
},
Ptr.case(DebugHTTPSServer) => {
this.ptr.as(DebugHTTPSServer).inspector_server_id = id;
if (this.ptr.as(DebugHTTPSServer).dev_server) |dev_server| {
dev_server.inspector_server_id = id;
}
},
else => bun.unreachablePanic("Invalid pointer tag", .{}),
}
}
pub fn inspectorServerID(this: AnyServer) jsc.Debugger.DebuggerId {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).inspector_server_id,
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).inspector_server_id,
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).inspector_server_id,
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).inspector_server_id,
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn plugins(this: AnyServer) ?*ServePlugins {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).plugins,
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).plugins,
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).plugins,
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).plugins,
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn getPlugins(this: AnyServer) PluginsResult {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).getPlugins(),
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).getPlugins(),
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).getPlugins(),
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).getPlugins(),
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn loadAndResolvePlugins(this: AnyServer, bundle: *HTMLBundle.HTMLBundleRoute, raw_plugins: []const []const u8, bunfig_path: []const u8) void {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).getPluginsAsync(bundle, raw_plugins, bunfig_path),
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).getPluginsAsync(bundle, raw_plugins, bunfig_path),
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).getPluginsAsync(bundle, raw_plugins, bunfig_path),
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).getPluginsAsync(bundle, raw_plugins, bunfig_path),
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
/// Returns:
/// - .ready if no plugin has to be loaded
/// - .err if there is a cached failure. Currently, this requires restarting the entire server.
/// - .pending if `callback` was stored. It will call `onPluginsResolved` or `onPluginsRejected` later.
pub fn getOrLoadPlugins(server: AnyServer, callback: ServePlugins.Callback) ServePlugins.GetOrStartLoadResult {
return switch (server.ptr.tag()) {
Ptr.case(HTTPServer) => server.ptr.as(HTTPServer).getOrLoadPlugins(callback),
Ptr.case(HTTPSServer) => server.ptr.as(HTTPSServer).getOrLoadPlugins(callback),
Ptr.case(DebugHTTPServer) => server.ptr.as(DebugHTTPServer).getOrLoadPlugins(callback),
Ptr.case(DebugHTTPSServer) => server.ptr.as(DebugHTTPSServer).getOrLoadPlugins(callback),
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn reloadStaticRoutes(this: AnyServer) !bool {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).reloadStaticRoutes(),
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).reloadStaticRoutes(),
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).reloadStaticRoutes(),
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).reloadStaticRoutes(),
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn appendStaticRoute(this: AnyServer, path: []const u8, route: AnyRoute, method: HTTP.Method.Optional) !void {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).appendStaticRoute(path, route, method),
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).appendStaticRoute(path, route, method),
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).appendStaticRoute(path, route, method),
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).appendStaticRoute(path, route, method),
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn globalThis(this: AnyServer) *jsc.JSGlobalObject {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).globalThis,
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).globalThis,
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).globalThis,
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).globalThis,
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn config(this: AnyServer) *const ServerConfig {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => &this.ptr.as(HTTPServer).config,
Ptr.case(HTTPSServer) => &this.ptr.as(HTTPSServer).config,
Ptr.case(DebugHTTPServer) => &this.ptr.as(DebugHTTPServer).config,
Ptr.case(DebugHTTPSServer) => &this.ptr.as(DebugHTTPSServer).config,
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn 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,
Ptr.case(DebugHTTPServer) => &this.ptr.as(DebugHTTPServer).config,
Ptr.case(DebugHTTPSServer) => &this.ptr.as(DebugHTTPSServer).config,
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
if (server_config.websocket == null) return null;
return &server_config.websocket.?.handler;
}
pub fn onRequest(
this: AnyServer,
req: *uws.Request,
resp: bun.uws.AnyResponse,
) void {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onRequest(req, resp.assertNoSSL()),
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onRequest(req, resp.assertSSL()),
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onRequest(req, resp.assertNoSSL()),
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onRequest(req, resp.assertSSL()),
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn from(server: anytype) AnyServer {
return .{ .ptr = Ptr.init(server) };
}
pub fn onPendingRequest(this: AnyServer) void {
switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onPendingRequest(),
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onPendingRequest(),
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onPendingRequest(),
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onPendingRequest(),
else => bun.unreachablePanic("Invalid pointer tag", .{}),
}
}
pub fn onRequestComplete(this: AnyServer) void {
switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onRequestComplete(),
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onRequestComplete(),
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onRequestComplete(),
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onRequestComplete(),
else => bun.unreachablePanic("Invalid pointer tag", .{}),
}
}
pub fn onStaticRequestComplete(this: AnyServer) void {
switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onStaticRequestComplete(),
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onStaticRequestComplete(),
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onStaticRequestComplete(),
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onStaticRequestComplete(),
else => bun.unreachablePanic("Invalid pointer tag", .{}),
}
}
pub fn publish(this: AnyServer, topic: []const u8, message: []const u8, opcode: uws.Opcode, compress: bool) bool {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).app.?.publish(topic, message, opcode, compress),
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).app.?.publish(topic, message, opcode, compress),
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).app.?.publish(topic, message, opcode, compress),
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).app.?.publish(topic, message, opcode, compress),
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn onRequestFromSaved(
this: AnyServer,
req: SavedRequest.Union,
resp: uws.AnyResponse,
callback: jsc.JSValue,
comptime extra_arg_count: usize,
extra_args: [extra_arg_count]JSValue,
) void {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onRequestFromSaved(req, resp.TCP, callback, extra_arg_count, extra_args),
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onRequestFromSaved(req, resp.SSL, callback, extra_arg_count, extra_args),
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onRequestFromSaved(req, resp.TCP, callback, extra_arg_count, extra_args),
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onRequestFromSaved(req, resp.SSL, callback, extra_arg_count, extra_args),
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn prepareAndSaveJsRequestContext(
server: AnyServer,
req: *uws.Request,
resp: uws.AnyResponse,
global: *jsc.JSGlobalObject,
method: ?bun.http.Method,
) ?SavedRequest {
return switch (server.ptr.tag()) {
Ptr.case(HTTPServer) => (server.ptr.as(HTTPServer).prepareJsRequestContext(req, resp.TCP, null, true, method) orelse return null).save(global, req, resp.TCP),
Ptr.case(HTTPSServer) => (server.ptr.as(HTTPSServer).prepareJsRequestContext(req, resp.SSL, null, true, method) orelse return null).save(global, req, resp.SSL),
Ptr.case(DebugHTTPServer) => (server.ptr.as(DebugHTTPServer).prepareJsRequestContext(req, resp.TCP, null, true, method) orelse return null).save(global, req, resp.TCP),
Ptr.case(DebugHTTPSServer) => (server.ptr.as(DebugHTTPSServer).prepareJsRequestContext(req, resp.SSL, null, true, method) orelse return null).save(global, req, resp.SSL),
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn numSubscribers(this: AnyServer, topic: []const u8) u32 {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).app.?.numSubscribers(topic),
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).app.?.numSubscribers(topic),
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).app.?.numSubscribers(topic),
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).app.?.numSubscribers(topic),
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn devServer(this: AnyServer) ?*bun.bake.DevServer {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).dev_server,
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).dev_server,
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).dev_server,
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).dev_server,
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
};
extern fn Bun__addInspector(bool, *anyopaque, *jsc.JSGlobalObject) void;
pub export fn Server__setIdleTimeout(server: jsc.JSValue, seconds: jsc.JSValue, globalThis: *jsc.JSGlobalObject) void {
Server__setIdleTimeout_(server, seconds, globalThis) catch |err| switch (err) {
error.JSError => {},
error.OutOfMemory => {
_ = globalThis.throwOutOfMemoryValue();
},
};
}
pub fn Server__setIdleTimeout_(server: jsc.JSValue, seconds: jsc.JSValue, globalThis: *jsc.JSGlobalObject) bun.JSError!void {
if (!server.isObject()) {
return globalThis.throw("Failed to set timeout: The 'this' value is not a Server.", .{});
}
if (!seconds.isNumber()) {
return globalThis.throw("Failed to set timeout: The provided value is not of type 'number'.", .{});
}
const value = seconds.to(c_uint);
if (server.as(HTTPServer)) |this| {
this.setIdleTimeout(value);
} else if (server.as(HTTPSServer)) |this| {
this.setIdleTimeout(value);
} else if (server.as(DebugHTTPServer)) |this| {
this.setIdleTimeout(value);
} else if (server.as(DebugHTTPSServer)) |this| {
this.setIdleTimeout(value);
} else {
return globalThis.throw("Failed to set timeout: The 'this' value is not a Server.", .{});
}
}
pub fn Server__setOnClientError_(globalThis: *jsc.JSGlobalObject, server: jsc.JSValue, callback: jsc.JSValue) bun.JSError!jsc.JSValue {
if (!server.isObject()) {
return globalThis.throw("Failed to set clientError: The 'this' value is not a Server.", .{});
}
if (!callback.isFunction()) {
return globalThis.throw("Failed to set clientError: The provided value is not a function.", .{});
}
if (server.as(HTTPServer)) |this| {
if (this.app) |app| {
this.on_clienterror.deinit();
this.on_clienterror = jsc.Strong.Optional.create(callback, globalThis);
app.onClientError(*HTTPServer, this, HTTPServer.onClientErrorCallback);
}
} else if (server.as(HTTPSServer)) |this| {
if (this.app) |app| {
this.on_clienterror.deinit();
this.on_clienterror = jsc.Strong.Optional.create(callback, globalThis);
app.onClientError(*HTTPSServer, this, HTTPSServer.onClientErrorCallback);
}
} else if (server.as(DebugHTTPServer)) |this| {
if (this.app) |app| {
this.on_clienterror.deinit();
this.on_clienterror = jsc.Strong.Optional.create(callback, globalThis);
app.onClientError(*DebugHTTPServer, this, DebugHTTPServer.onClientErrorCallback);
}
} else if (server.as(DebugHTTPSServer)) |this| {
if (this.app) |app| {
this.on_clienterror.deinit();
this.on_clienterror = jsc.Strong.Optional.create(callback, globalThis);
app.onClientError(*DebugHTTPSServer, this, DebugHTTPSServer.onClientErrorCallback);
}
} else {
bun.debugAssert(false);
}
return .js_undefined;
}
pub fn Server__setAppFlags_(globalThis: *jsc.JSGlobalObject, server: jsc.JSValue, require_host_header: bool, use_strict_method_validation: bool) bun.JSError!jsc.JSValue {
if (!server.isObject()) {
return globalThis.throw("Failed to set requireHostHeader: The 'this' value is not a Server.", .{});
}
if (server.as(HTTPServer)) |this| {
this.setFlags(require_host_header, use_strict_method_validation);
} else if (server.as(HTTPSServer)) |this| {
this.setFlags(require_host_header, use_strict_method_validation);
} else if (server.as(DebugHTTPServer)) |this| {
this.setFlags(require_host_header, use_strict_method_validation);
} else if (server.as(DebugHTTPSServer)) |this| {
this.setFlags(require_host_header, use_strict_method_validation);
} else {
return globalThis.throw("Failed to set timeout: The 'this' value is not a Server.", .{});
}
return .js_undefined;
}
pub fn Server__setMaxHTTPHeaderSize_(globalThis: *jsc.JSGlobalObject, server: jsc.JSValue, max_header_size: u64) bun.JSError!jsc.JSValue {
if (!server.isObject()) {
return globalThis.throw("Failed to set maxHeaderSize: The 'this' value is not a Server.", .{});
}
if (server.as(HTTPServer)) |this| {
this.setMaxHTTPHeaderSize(max_header_size);
} else if (server.as(HTTPSServer)) |this| {
this.setMaxHTTPHeaderSize(max_header_size);
} else if (server.as(DebugHTTPServer)) |this| {
this.setMaxHTTPHeaderSize(max_header_size);
} else if (server.as(DebugHTTPSServer)) |this| {
this.setMaxHTTPHeaderSize(max_header_size);
} else {
return globalThis.throw("Failed to set maxHeaderSize: The 'this' value is not a Server.", .{});
}
return .js_undefined;
}
comptime {
_ = Server__setIdleTimeout;
_ = NodeHTTPResponse.create;
@export(&jsc.host_fn.wrap4(Server__setAppFlags_), .{ .name = "Server__setAppFlags" });
@export(&jsc.host_fn.wrap3(Server__setOnClientError_), .{ .name = "Server__setOnClientError" });
@export(&jsc.host_fn.wrap3(Server__setMaxHTTPHeaderSize_), .{ .name = "Server__setMaxHTTPHeaderSize" });
}
extern fn NodeHTTPServer__onRequest_http(
any_server: usize,
globalThis: *jsc.JSGlobalObject,
this: jsc.JSValue,
callback: jsc.JSValue,
methodString: jsc.JSValue,
request: *uws.Request,
response: *uws.NewApp(false).Response,
upgrade_ctx: ?*uws.SocketContext,
node_response_ptr: *?*NodeHTTPResponse,
) jsc.JSValue;
extern fn NodeHTTPServer__onRequest_https(
any_server: usize,
globalThis: *jsc.JSGlobalObject,
this: jsc.JSValue,
callback: jsc.JSValue,
methodString: jsc.JSValue,
request: *uws.Request,
response: *uws.NewApp(true).Response,
upgrade_ctx: ?*uws.SocketContext,
node_response_ptr: *?*NodeHTTPResponse,
) 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;
extern "c" fn Bun__ServerRouteList__callRoute(
globalObject: *jsc.JSGlobalObject,
index: u32,
requestPtr: *Request,
serverObject: jsc.JSValue,
routeListObject: jsc.JSValue,
requestObject: *jsc.JSValue,
req: *uws.Request,
) jsc.JSValue;
extern "c" fn Bun__ServerRouteList__create(
globalObject: *jsc.JSGlobalObject,
callbacks: [*]jsc.JSValue,
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;
}
const string = []const u8;
const Sys = @import("../../sys.zig");
const options = @import("../../options.zig");
const std = @import("std");
const URL = @import("../../url.zig").URL;
const Allocator = std.mem.Allocator;
const Runtime = @import("../../runtime.zig");
const Fallback = Runtime.Fallback;
const bun = @import("bun");
const Async = bun.Async;
const Environment = bun.Environment;
const Global = bun.Global;
const Output = bun.Output;
const Transpiler = bun.Transpiler;
const analytics = bun.analytics;
const assert = bun.assert;
const default_allocator = bun.default_allocator;
const js_printer = bun.js_printer;
const logger = bun.logger;
const strings = bun.strings;
const uws = bun.uws;
const Arena = bun.allocators.MimallocArena;
const BoringSSL = bun.BoringSSL.c;
const SocketAddress = bun.api.socket.SocketAddress;
const HTTP = bun.http;
const MimeType = HTTP.MimeType;
const jsc = bun.jsc;
const JSGlobalObject = bun.jsc.JSGlobalObject;
const JSPromise = bun.jsc.JSPromise;
const JSValue = bun.jsc.JSValue;
const Node = bun.jsc.Node;
const VM = bun.jsc.VM;
const VirtualMachine = jsc.VirtualMachine;
const ZigString = bun.jsc.ZigString;
const host_fn = jsc.host_fn;
const WebCore = bun.jsc.WebCore;
const Blob = jsc.WebCore.Blob;
const Fetch = WebCore.Fetch;
const Headers = WebCore.Headers;
const Request = WebCore.Request;
const Response = WebCore.Response;