Files
bun.sh/src/bun.js/api/server.zig
2025-04-23 19:28:37 -07:00

7793 lines
323 KiB
Zig

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