From 1bed7a7fd1b9333a206eff3aa67e3a953807b9b8 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 30 Aug 2024 01:36:18 -0700 Subject: [PATCH] Introduce `static` option in Bun.serve() (#13540) --- docs/api/http.md | 22 ++ packages/bun-types/bun.d.ts | 20 ++ src/bun.js/api/server.zig | 556 +++++++++++++++++++++++++++----- src/bun.js/webcore/response.zig | 5 + src/bun.js/webcore/streams.zig | 2 +- src/deps/libuwsockets.cpp | 11 +- src/deps/uws.zig | 188 +++++++++-- 7 files changed, 706 insertions(+), 98 deletions(-) diff --git a/docs/api/http.md b/docs/api/http.md index 1f873dbb5d012a..d2ee381e678d25 100644 --- a/docs/api/http.md +++ b/docs/api/http.md @@ -70,6 +70,28 @@ const server = Bun.serve({ }); ``` +### `static` responses + +Serve static responses by route with the `static` option + +```ts +Bun.serve({ + static: { + "/api/health-check": new Response("All good!"), + "/old-link": Response.redirect("/new-link", 301), + "/": new Response("Hello World"), + }, + + fetch(req) { + return new Response("404!"); + }, +}); +``` + +{% note %} +`static` is experimental and may change in the future. +{% /note %} + ### Changing the `port` and `hostname` To configure which port and hostname the server will listen on, set `port` and `hostname` in the options object. diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 99fd9de88efd04..f717f08ddc1ece 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -2298,6 +2298,26 @@ declare module "bun" { * This string will currently do nothing. But in the future it could be useful for logs or metrics. */ id?: string | null; + + /** + * Server static Response objects by route. + * + * @example + * ```ts + * Bun.serve({ + * static: { + * "/": new Response("Hello World"), + * "/about": new Response("About"), + * }, + * fetch(req) { + * return new Response("Fallback response"); + * }, + * }); + * ``` + * + * @experimental + */ + static?: Record<`/${string}`, Response>; } interface ServeOptions extends GenericServeOptions { diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 0eaadb692f5b6d..3d953182dcd69a 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -2,7 +2,7 @@ const Bun = @This(); const default_allocator = bun.default_allocator; const bun = @import("root").bun; const Environment = bun.Environment; - +const AnyBlob = bun.JSC.WebCore.AnyBlob; const Global = bun.Global; const strings = bun.strings; const string = bun.string; @@ -90,7 +90,7 @@ const SendfileContext = struct { const linux = std.os.linux; const Async = bun.Async; const httplog = Output.scoped(.Server, false); - +const ctxLog = Output.scoped(.RequestContext, false); const BlobFileContentResult = struct { data: [:0]const u8, fn init(comptime fieldname: []const u8, js_obj: JSC.JSValue, global: *JSC.JSGlobalObject, exception: JSC.C.ExceptionRef) ?BlobFileContentResult { @@ -119,6 +119,296 @@ const BlobFileContentResult = struct { } }; +fn getContentType(headers: ?*JSC.FetchHeaders, blob: *const JSC.WebCore.AnyBlob, 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 writeHeaders( + headers: *JSC.FetchHeaders, + comptime ssl: bool, + resp_ptr: ?*uws.NewApp(ssl).Response, +) void { + ctxLog("writeHeaders", .{}); + headers.fastRemove(.ContentLength); + headers.fastRemove(.TransferEncoding); + if (!ssl) headers.fastRemove(.StrictTransportSecurity); + if (resp_ptr) |resp| { + headers.toUWSResponse(ssl, resp); + } +} + +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); + } + } +} + +const StaticRoute = struct { + server: ?AnyServer = null, + status_code: u16, + blob: AnyBlob, + cached_blob_size: u64 = 0, + has_content_disposition: bool = false, + headers: Headers = .{ + .allocator = bun.default_allocator, + }, + ref_count: u32 = 1, + + const HTTPResponse = uws.AnyResponse; + const Route = @This(); + + pub usingnamespace bun.NewRefCounted(@This(), deinit); + + fn deinit(this: *Route) void { + this.blob.detach(); + this.headers.deinit(); + + this.destroy(); + } + + pub fn fromJS(globalThis: *JSC.JSGlobalObject, argument: JSC.JSValue) ?*Route { + if (argument.as(JSC.WebCore.Response)) |response| { + + // The user may want to pass in the same Response object multiple endpoints + // Let's let them do that. + response.body.value.toBlobIfPossible(); + + var blob: AnyBlob = brk: { + switch (response.body.value) { + .Used => { + globalThis.throwInvalidArguments("Response body has already been used", .{}); + return null; + }, + + else => { + globalThis.throwInvalidArguments("Body must be fully buffered before it can be used in a static route. Consider calling new Response(await response.blob()) to buffer the body.", .{}); + return null; + }, + .Null, .Empty => { + break :brk AnyBlob{ + .InternalBlob = JSC.WebCore.InternalBlob{ + .bytes = std.ArrayList(u8).init(bun.default_allocator), + }, + }; + }, + + .Blob, .InternalBlob, .WTFStringImpl => { + if (response.body.value == .Blob and response.body.value.Blob.needsToReadFile()) { + globalThis.throwTODO("TODO: support Bun.file(path) in static routes"); + return null; + } + var blob = response.body.value.use(); + blob.globalThis = globalThis; + blob.allocator = null; + response.body.value = .{ .Blob = blob.dupe() }; + + break :brk .{ .Blob = blob }; + }, + } + }; + + var has_content_disposition = false; + + if (response.init.headers) |headers| { + has_content_disposition = headers.fastHas(.ContentDisposition); + headers.fastRemove(.TransferEncoding); + headers.fastRemove(.ContentLength); + } + + const headers: Headers = if (response.init.headers) |headers| + Headers.from(headers, bun.default_allocator, .{ + .body = &blob, + }) catch { + blob.detach(); + globalThis.throwOutOfMemory(); + return null; + } + else + .{ + .allocator = bun.default_allocator, + }; + + return Route.new(.{ + .blob = blob, + .cached_blob_size = blob.size(), + .has_content_disposition = has_content_disposition, + .headers = headers, + .server = null, + .status_code = response.statusCode(), + }); + } + + globalThis.throwInvalidArguments("Expected a Response object", .{}); + return null; + } + + // HEAD requests have no body. + pub fn onHEADRequest(this: *Route, req: *uws.Request, resp: HTTPResponse) void { + req.setYield(false); + this.ref(); + if (this.server) |server| { + server.onPendingRequest(); + resp.timeout(server.config().idleTimeout); + } + resp.corked(renderMetadata, .{ this, resp }); + resp.end("", resp.shouldCloseConnection()); + this.onResponseComplete(resp); + } + + pub fn onRequest(this: *Route, req: *uws.Request, resp: HTTPResponse) void { + req.setYield(false); + this.ref(); + if (this.server) |server| { + server.onPendingRequest(); + resp.timeout(server.config().idleTimeout); + } + var finished = false; + this.doRenderBlob(resp, &finished); + if (finished) { + this.onResponseComplete(resp); + return; + } + + this.toAsync(resp); + } + + fn toAsync(this: *Route, resp: HTTPResponse) void { + resp.onAborted(*Route, onAborted, this); + resp.onWritable(*Route, onWritableBytes, this); + } + + fn onAborted(this: *Route, resp: HTTPResponse) void { + this.onResponseComplete(resp); + } + + fn onResponseComplete(this: *Route, resp: HTTPResponse) void { + resp.clearAborted(); + resp.clearOnWritable(); + + if (this.server) |server| { + server.onStaticRequestComplete(); + } + + this.deref(); + } + + pub fn doRenderBlob(this: *Route, resp: HTTPResponse, did_finish: *bool) 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.blob.fastSize() < 16384 - 1024) { + resp.corked(doRenderBlobCorked, .{ this, resp, did_finish }); + } else { + this.doRenderBlobCorked(resp, did_finish); + } + } + + pub fn doRenderBlobCorked(this: *Route, resp: HTTPResponse, did_finish: *bool) void { + this.renderMetadata(resp); + this.renderBytes(resp, did_finish); + } + + fn onWritable(this: *Route, write_offset: u64, resp: HTTPResponse) void { + if (!this.onWritableBytes(write_offset, resp)) { + this.toAsync(resp); + return; + } + + this.onResponseComplete(resp); + } + + fn onWritableBytes(this: *Route, write_offset: u64, resp: HTTPResponse) bool { + const blob = this.blob; + const all_bytes = blob.slice(); + + const bytes = all_bytes[@min(all_bytes.len, @as(usize, @truncate(write_offset)))..]; + + if (!resp.tryEnd( + bytes, + all_bytes.len, + resp.shouldCloseConnection(), + )) { + return false; + } + + return true; + } + + fn doWriteStatus(_: *StaticRoute, status: u16, resp: HTTPResponse) void { + switch (resp) { + .SSL => |r| writeStatus(true, r, status), + .TCP => |r| writeStatus(false, r, status), + } + } + + fn doWriteHeaders(this: *StaticRoute, resp: HTTPResponse) void { + switch (resp) { + inline .SSL, .TCP => |s| { + const entries = this.headers.entries.slice(); + const names: []const Api.StringPointer = entries.items(.name); + const values: []const Api.StringPointer = entries.items(.value); + const buf = this.headers.buf.items; + + for (names, values) |name, value| { + s.writeHeader(name.slice(buf), value.slice(buf)); + } + }, + } + } + + fn renderBytes(this: *Route, resp: HTTPResponse, did_finish: *bool) void { + did_finish.* = this.onWritableBytes(0, resp); + } + + fn renderMetadata(this: *Route, resp: HTTPResponse) void { + var status = this.status_code; + const size = this.cached_blob_size; + + status = if (status == 200 and size == 0 and !this.blob.isDetached()) + 204 + else + status; + + this.doWriteStatus(status, resp); + this.doWriteHeaders(resp); + } +}; + pub const ServerConfig = struct { address: union(enum) { tcp: struct { @@ -163,6 +453,41 @@ pub const ServerConfig = struct { id: []const u8 = "", allow_hot: bool = true, + static_routes: std.ArrayList(StaticRouteEntry) = std.ArrayList(StaticRouteEntry).init(bun.default_allocator), + + pub const StaticRouteEntry = struct { + path: []const u8, + route: *StaticRoute, + + pub fn deinit(this: *StaticRouteEntry) void { + bun.default_allocator.free(this.path); + this.route.deref(); + } + }; + + pub fn applyStaticRoutes(this: *ServerConfig, comptime ssl: bool, server: AnyServer, app: *uws.NewApp(ssl)) void { + for (this.static_routes.items) |entry| { + entry.route.server = server; + const handler_wrap = struct { + pub fn handler(route: *StaticRoute, 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: *StaticRoute, req: *uws.Request, resp: *uws.NewApp(ssl).Response) void { + route.onHEADRequest(req, switch (comptime ssl) { + true => .{ .SSL = resp }, + false => .{ .TCP = resp }, + }); + } + }; + app.head(entry.path, *StaticRoute, entry.route, handler_wrap.HEAD); + app.any(entry.path, *StaticRoute, entry.route, handler_wrap.handler); + } + } + pub fn deinit(this: *ServerConfig) void { this.address.deinit(bun.default_allocator); @@ -181,6 +506,11 @@ pub const ServerConfig = struct { this.sni.?.deinitWithAllocator(bun.default_allocator); this.sni = null; } + + for (this.static_routes.items) |*entry| { + entry.deinit(); + } + this.static_routes.clearAndFree(); } pub fn computeID(this: *const ServerConfig, allocator: std.mem.Allocator) []const u8 { @@ -886,14 +1216,66 @@ pub const ServerConfig = struct { args.base_uri = origin; } - if (arguments.next()) |arg| { - defer if (global.hasException()) if (args.ssl_config) |*conf| conf.deinit(); + defer { + if (global.hasException() or exception.* != null) { + if (args.ssl_config) |*conf| { + conf.deinit(); + args.ssl_config = null; + } + } + } + if (arguments.next()) |arg| { if (!arg.isObject()) { JSC.throwInvalidArguments("Bun.serve expects an object", .{}, global, exception); return args; } + if (arg.get(global, "static")) |static| { + if (!static.isObject()) { + JSC.throwInvalidArguments("Bun.serve expects 'static' to be an object shaped like { [pathname: string]: Response }", .{}, global, exception); + return args; + } + + var iter = JSC.JSPropertyIterator(.{ + .skip_empty_name = true, + .include_value = true, + }).init(global, static); + defer iter.deinit(); + + while (iter.next()) |key| { + const path, const is_ascii = key.toOwnedSliceReturningAllASCII(bun.default_allocator) catch bun.outOfMemory(); + + const value = iter.value; + + if (path.len == 0 or path[0] != '/') { + bun.default_allocator.free(path); + JSC.throwInvalidArguments("Invalid static route \"{s}\". path must start with '/'", .{path}, global, exception); + return args; + } + + if (!is_ascii) { + bun.default_allocator.free(path); + JSC.throwInvalidArguments("Invalid static route \"{s}\". Please encode all non-ASCII characters in the path.", .{path}, global, exception); + return args; + } + + if (StaticRoute.fromJS(global, value)) |route| { + args.static_routes.append(.{ + .path = path, + .route = route, + }) catch bun.outOfMemory(); + } else if (global.hasException()) { + bun.default_allocator.free(path); + return args; + } else { + Output.panic("Internal error: expected exception or static route", .{}); + } + } + } + + if (global.hasException()) return args; + if (arg.get(global, "idleTimeout")) |value| { if (!value.isUndefinedOrNull()) { if (!value.isAnyInt()) { @@ -1420,7 +1802,7 @@ pub const AnyRequestContext = struct { fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comptime ThisServer: type) type { return struct { const RequestContext = @This(); - const ctxLog = Output.scoped(.RequestContext, false); + const App = uws.NewApp(ssl_enabled); pub threadlocal var pool: ?*RequestContext.RequestContextStackAllocator = null; pub const ResponseStream = JSC.WebCore.HTTPServerWritable(ssl_enabled); @@ -1861,7 +2243,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } } - pub fn onWritableResponseBuffer(this: *RequestContext, _: u64, resp: *App.Response) callconv(.C) bool { + pub fn onWritableResponseBuffer(this: *RequestContext, _: u64, resp: *App.Response) bool { ctxLog("onWritableResponseBuffer", .{}); assert(this.resp == resp); @@ -1873,7 +2255,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } // TODO: should we cork? - pub fn onWritableCompleteResponseBufferAndMetadata(this: *RequestContext, write_offset: u64, resp: *App.Response) callconv(.C) bool { + pub fn onWritableCompleteResponseBufferAndMetadata(this: *RequestContext, write_offset: u64, resp: *App.Response) bool { ctxLog("onWritableCompleteResponseBufferAndMetadata", .{}); assert(this.resp == resp); @@ -1893,7 +2275,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp return this.sendWritableBytesForCompleteResponseBuffer(this.response_buf_owned.items, write_offset, resp); } - pub fn onWritableCompleteResponseBuffer(this: *RequestContext, write_offset: u64, resp: *App.Response) callconv(.C) bool { + pub fn onWritableCompleteResponseBuffer(this: *RequestContext, write_offset: u64, resp: *App.Response) bool { ctxLog("onWritableCompleteResponseBuffer", .{}); assert(this.resp == resp); if (this.isAbortedOrEnded()) { @@ -2027,33 +2409,6 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } } - fn writeHeaders( - this: *RequestContext, - headers: *JSC.FetchHeaders, - ) void { - ctxLog("writeHeaders", .{}); - headers.fastRemove(.ContentLength); - headers.fastRemove(.TransferEncoding); - if (!ssl_enabled) headers.fastRemove(.StrictTransportSecurity); - if (this.resp) |resp| { - headers.toUWSResponse(ssl_enabled, resp); - } - } - - pub fn writeStatus(this: *RequestContext, status: u16) void { - var status_text_buf: [48]u8 = undefined; - assert(!this.flags.has_written_status); - this.flags.has_written_status = true; - - if (this.resp) |resp| { - if (HTTPStatusText.get(status)) |text| { - resp.writeStatus(text); - } else { - resp.writeStatus(std.fmt.bufPrint(&status_text_buf, "{d} HM", .{status}) catch unreachable); - } - } - } - pub fn endSendFile(this: *RequestContext, writeOffSet: usize, closeConnection: bool) void { if (this.resp) |resp| { defer this.deref(); @@ -2142,7 +2497,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp return true; } - pub fn onWritableBytes(this: *RequestContext, write_offset: u64, resp: *App.Response) callconv(.C) bool { + pub fn onWritableBytes(this: *RequestContext, write_offset: u64, resp: *App.Response) bool { ctxLog("onWritableBytes", .{}); assert(this.resp == resp); if (this.isAbortedOrEnded()) { @@ -2192,7 +2547,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp return true; } - pub fn onWritableSendfile(this: *RequestContext, _: u64, _: *App.Response) callconv(.C) bool { + pub fn onWritableSendfile(this: *RequestContext, _: u64, _: *App.Response) bool { ctxLog("onWritableSendfile", .{}); return this.onSendfile(); } @@ -3158,7 +3513,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp pub inline fn shouldCloseConnection(this: *const RequestContext) bool { if (this.resp) |resp| { - return resp.state().isHttpConnectionClose(); + return resp.shouldCloseConnection(); } return false; } @@ -3324,33 +3679,11 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp else status; - var needs_content_type = true; - var content_type_needs_free = false; - - const content_type: MimeType = brk: { - if (response.init.headers) |headers_| { - if (headers_.fastGet(.ContentType)) |content| { - needs_content_type = false; - - var content_slice = content.toSlice(this.allocator); - defer content_slice.deinit(); - - const content_type_allocator = if (content_slice.allocator.isNull()) null else this.allocator; - break :brk MimeType.init(content_slice.slice(), content_type_allocator, &content_type_needs_free); - } - } - - break :brk if (this.blob.contentType().len > 0) - MimeType.byName(this.blob.contentType()) - else if (MimeType.sniff(this.blob.slice())) |content| - content - else if (this.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; - }; + 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; @@ -3362,15 +3695,15 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp status = 206; } - this.writeStatus(status); - this.writeHeaders(headers_); + this.doWriteStatus(status); + this.doWriteHeaders(headers_); response.init.headers = null; headers_.deref(); } else if (needs_content_range) { status = 206; - this.writeStatus(status); + this.doWriteStatus(status); } else { - this.writeStatus(status); + this.doWriteStatus(status); } if (needs_content_type and @@ -3421,6 +3754,17 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } } + 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: *JSC.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; @@ -4328,7 +4672,7 @@ pub const ServerWebSocket = struct { return null; } - pub fn finalize(this: *ServerWebSocket) callconv(.C) void { + pub fn finalize(this: *ServerWebSocket) void { log("finalize", .{}); this.destroy(); } @@ -5634,6 +5978,18 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp this.config.websocket = ws.*; } // we don't remove it } + + if (this.config.static_routes.items.len > 0) { + // TODO: clear old static routes + } + + if (new_config.static_routes.items.len > 0) { + new_config.applyStaticRoutes( + ssl_enabled, + AnyServer.from(this), + this.app, + ); + } } pub fn onReload( @@ -5964,6 +6320,11 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp 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(); @@ -5971,7 +6332,7 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp this.deinitIfWeCan(); } - pub fn finalize(this: *ThisServer) callconv(.C) void { + pub fn finalize(this: *ThisServer) void { httplog("finalize", .{}); this.flags.has_js_deinited = true; this.deinitIfWeCan(); @@ -6311,13 +6672,17 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp } } + pub fn onPendingRequest(this: *ThisServer) void { + this.pending_requests += 1; + } + pub fn onRequest( this: *ThisServer, req: *uws.Request, resp: *App.Response, ) void { JSC.markBinding(@src()); - this.pending_requests += 1; + this.onPendingRequest(); if (comptime Environment.isDebug) { this.vm.eventLoop().debug.enter(); } @@ -6505,6 +6870,14 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp } fn setRoutes(this: *ThisServer) void { + if (this.config.static_routes.items.len > 0) { + this.config.applyStaticRoutes( + ssl_enabled, + AnyServer.from(this), + this.app, + ); + } + if (this.config.websocket) |*websocket| { websocket.globalObject = this.globalThis; websocket.handler.app = this.app; @@ -6641,7 +7014,46 @@ pub const HTTPServer = NewServer(JSC.Codegen.JSHTTPServer, false, false); pub const HTTPSServer = NewServer(JSC.Codegen.JSHTTPSServer, true, false); pub const DebugHTTPServer = NewServer(JSC.Codegen.JSDebugHTTPServer, false, true); pub const DebugHTTPSServer = NewServer(JSC.Codegen.JSDebugHTTPSServer, true, true); +const AnyServer = union(enum) { + HTTPServer: *HTTPServer, + HTTPSServer: *HTTPSServer, + DebugHTTPServer: *DebugHTTPServer, + DebugHTTPSServer: *DebugHTTPSServer, + + pub fn config(this: AnyServer) *const ServerConfig { + return switch (this) { + inline else => |server| &server.config, + }; + } + + pub fn from(server: anytype) AnyServer { + return switch (@TypeOf(server)) { + *HTTPServer => AnyServer{ .HTTPServer = server }, + *HTTPSServer => AnyServer{ .HTTPSServer = server }, + *DebugHTTPServer => AnyServer{ .DebugHTTPServer = server }, + *DebugHTTPSServer => AnyServer{ .DebugHTTPSServer = server }, + else => @compileError("Invalid server type"), + }; + } + + pub fn onPendingRequest(this: AnyServer) void { + switch (this) { + inline else => |server| server.onPendingRequest(), + } + } + pub fn onRequestComplete(this: AnyServer) void { + switch (this) { + inline else => |server| server.onRequestComplete(), + } + } + + pub fn onStaticRequestComplete(this: AnyServer) void { + switch (this) { + inline else => |server| server.onStaticRequestComplete(), + } + } +}; const welcome_page_html_gz = @embedFile("welcome-page.html.gz"); extern fn Bun__addInspector(bool, *anyopaque, *JSC.JSGlobalObject) void; diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index cc8002bdc84f85..8e9763245e45cc 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -2997,6 +2997,11 @@ pub const Headers = struct { buf: std.ArrayListUnmanaged(u8) = .{}, allocator: std.mem.Allocator, + pub fn deinit(this: *Headers) void { + this.entries.deinit(this.allocator); + this.buf.clearAndFree(this.allocator); + } + pub fn asStr(this: *const Headers, ptr: Api.StringPointer) []const u8 { return if (ptr.offset + ptr.length <= this.buf.items.len) this.buf.items[ptr.offset..][0..ptr.length] diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index a41e266f334ebf..4e02f13acec137 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -2120,7 +2120,7 @@ pub fn HTTPServerWritable(comptime ssl: bool) type { return this.buffer.ptr[this.offset..this.buffer.len]; } - pub fn onWritable(this: *@This(), write_offset: u64, _: *UWSResponse) callconv(.C) bool { + pub fn onWritable(this: *@This(), write_offset: u64, _: *UWSResponse) bool { // write_offset is the amount of data that was written not how much we need to write log("onWritable ({d})", .{write_offset}); // onWritable reset backpressure state to allow flushing diff --git a/src/deps/libuwsockets.cpp b/src/deps/libuwsockets.cpp index 3e3d527096dd26..b25d7d1e74f7c1 100644 --- a/src/deps/libuwsockets.cpp +++ b/src/deps/libuwsockets.cpp @@ -183,8 +183,9 @@ extern "C" } } - void uws_app_head(int ssl, uws_app_t *app, const char *pattern, uws_method_handler handler, void *user_data) + void uws_app_head(int ssl, uws_app_t *app, const char *pattern_ptr, size_t pattern_len, uws_method_handler handler, void *user_data) { + std::string pattern = std::string(pattern_ptr, pattern_len); if (ssl) { uWS::SSLApp *uwsApp = (uWS::SSLApp *)app; @@ -194,7 +195,7 @@ extern "C" return; } uwsApp->head(pattern, [handler, user_data](auto *res, auto *req) - { handler((uws_res_t *)res, (uws_req_t *)req, user_data); }); + { handler((uws_res_t *)res, (uws_req_t *)req, user_data); }); } else { @@ -205,10 +206,9 @@ extern "C" return; } uwsApp->head(pattern, [handler, user_data](auto *res, auto *req) - { handler((uws_res_t *)res, (uws_req_t *)req, user_data); }); + { handler((uws_res_t *)res, (uws_req_t *)req, user_data); }); } } - void uws_app_connect(int ssl, uws_app_t *app, const char *pattern, uws_method_handler handler, void *user_data) { if (ssl) @@ -261,8 +261,9 @@ extern "C" } } - void uws_app_any(int ssl, uws_app_t *app, const char *pattern, uws_method_handler handler, void *user_data) + void uws_app_any(int ssl, uws_app_t *app, const char *pattern_ptr, size_t pattern_len, uws_method_handler handler, void *user_data) { + std::string pattern = std::string(pattern_ptr, pattern_len); if (ssl) { uWS::SSLApp *uwsApp = (uWS::SSLApp *)app; diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 5bfa67193f7a14..1bbaba9771c94d 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -1894,6 +1894,153 @@ pub const SocketAddress = struct { is_ipv6: bool, }; +pub const AnyResponse = union(enum) { + SSL: *NewApp(true).Response, + TCP: *NewApp(false).Response, + + pub fn timeout(this: AnyResponse, seconds: u8) void { + switch (this) { + .SSL => |resp| resp.timeout(seconds), + .TCP => |resp| resp.timeout(seconds), + } + } + + pub fn writeStatus(this: AnyResponse, status: []const u8) void { + return switch (this) { + .SSL => |resp| resp.writeStatus(status), + .TCP => |resp| resp.writeStatus(status), + }; + } + + pub fn writeHeader(this: AnyResponse, key: []const u8, value: []const u8) void { + return switch (this) { + .SSL => |resp| resp.writeHeader(key, value), + .TCP => |resp| resp.writeHeader(key, value), + }; + } + + pub fn write(this: AnyResponse, data: []const u8) void { + return switch (this) { + .SSL => |resp| resp.write(data), + .TCP => |resp| resp.write(data), + }; + } + + pub fn end(this: AnyResponse, data: []const u8, close_connection: bool) void { + return switch (this) { + .SSL => |resp| resp.end(data, close_connection), + .TCP => |resp| resp.end(data, close_connection), + }; + } + + pub fn shouldCloseConnection(this: AnyResponse) bool { + return switch (this) { + .SSL => |resp| resp.shouldCloseConnection(), + .TCP => |resp| resp.shouldCloseConnection(), + }; + } + + pub fn tryEnd(this: AnyResponse, data: []const u8, total_size: usize, close_connection: bool) bool { + return switch (this) { + .SSL => |resp| resp.tryEnd(data, total_size, close_connection), + .TCP => |resp| resp.tryEnd(data, total_size, close_connection), + }; + } + + pub fn pause(this: AnyResponse) void { + return switch (this) { + .SSL => |resp| resp.pause(), + .TCP => |resp| resp.pause(), + }; + } + + pub fn @"resume"(this: AnyResponse) void { + return switch (this) { + .SSL => |resp| resp.@"resume"(), + .TCP => |resp| resp.@"resume"(), + }; + } + + pub fn writeOrEndWithoutBody(this: AnyResponse, data: []const u8) void { + return switch (this) { + .SSL => |resp| resp.writeOrEndWithoutBody(data), + .TCP => |resp| resp.writeOrEndWithoutBody(data), + }; + } + + pub fn onWritable(this: AnyResponse, comptime UserDataType: type, comptime handler: fn (UserDataType, u64, AnyResponse) bool, opcional_data: UserDataType) void { + const wrapper = struct { + pub fn ssl_handler(user_data: UserDataType, offset: u64, resp: *NewApp(true).Response) bool { + return handler(user_data, offset, .{ .SSL = resp }); + } + + pub fn tcp_handler(user_data: UserDataType, offset: u64, resp: *NewApp(false).Response) bool { + return handler(user_data, offset, .{ .TCP = resp }); + } + }; + return switch (this) { + .SSL => |resp| resp.onWritable(UserDataType, wrapper.ssl_handler, opcional_data), + .TCP => |resp| resp.onWritable(UserDataType, wrapper.tcp_handler, opcional_data), + }; + } + + pub fn onAborted(this: AnyResponse, comptime UserDataType: type, comptime handler: fn (UserDataType, AnyResponse) void, opcional_data: UserDataType) void { + const wrapper = struct { + pub fn ssl_handler(user_data: UserDataType, resp: *NewApp(true).Response) void { + handler(user_data, .{ .SSL = resp }); + } + pub fn tcp_handler(user_data: UserDataType, resp: *NewApp(false).Response) void { + handler(user_data, .{ .TCP = resp }); + } + }; + return switch (this) { + .SSL => |resp| resp.onAborted(UserDataType, wrapper.ssl_handler, opcional_data), + .TCP => |resp| resp.onAborted(UserDataType, wrapper.tcp_handler, opcional_data), + }; + } + + pub fn clearAborted(this: AnyResponse) void { + return switch (this) { + .SSL => |resp| resp.clearAborted(), + .TCP => |resp| resp.clearAborted(), + }; + } + + pub fn clearOnWritable(this: AnyResponse) void { + return switch (this) { + .SSL => |resp| resp.clearOnWritable(), + .TCP => |resp| resp.clearOnWritable(), + }; + } + + pub fn clearOnData(this: AnyResponse) void { + return switch (this) { + .SSL => |resp| resp.clearOnData(), + .TCP => |resp| resp.clearOnData(), + }; + } + + pub fn endStream(this: AnyResponse, close_connection: bool) void { + return switch (this) { + .SSL => |resp| resp.endStream(close_connection), + .TCP => |resp| resp.endStream(close_connection), + }; + } + + pub fn corked(this: AnyResponse, comptime handler: anytype, args_tuple: anytype) void { + return switch (this) { + .SSL => |resp| resp.corked(handler, args_tuple), + .TCP => |resp| resp.corked(handler, args_tuple), + }; + } + + pub fn runCorkedWithType(this: AnyResponse, comptime UserDataType: type, comptime handler: fn (UserDataType) void, opcional_data: UserDataType) void { + return switch (this) { + .SSL => |resp| resp.runCorkedWithType(UserDataType, handler, opcional_data), + .TCP => |resp| resp.runCorkedWithType(UserDataType, handler, opcional_data), + }; + } +}; pub fn NewApp(comptime ssl: bool) type { return opaque { const ssl_flag = @as(i32, @intFromBool(ssl)); @@ -2046,7 +2193,7 @@ pub fn NewApp(comptime ssl: bool) type { } pub fn head( app: *ThisApp, - pattern: [:0]const u8, + pattern: []const u8, comptime UserDataType: type, user_data: UserDataType, comptime handler: (fn (UserDataType, *Request, *Response) void), @@ -2054,7 +2201,7 @@ pub fn NewApp(comptime ssl: bool) type { if (comptime is_bindgen) { unreachable; } - uws_app_head(ssl_flag, @as(*uws_app_t, @ptrCast(app)), pattern, RouteHandler(UserDataType, handler).handle, user_data); + uws_app_head(ssl_flag, @as(*uws_app_t, @ptrCast(app)), pattern.ptr, pattern.len, RouteHandler(UserDataType, handler).handle, user_data); } pub fn connect( app: *ThisApp, @@ -2082,7 +2229,7 @@ pub fn NewApp(comptime ssl: bool) type { } pub fn any( app: *ThisApp, - pattern: [:0]const u8, + pattern: []const u8, comptime UserDataType: type, user_data: UserDataType, comptime handler: (fn (UserDataType, *Request, *Response) void), @@ -2090,7 +2237,7 @@ pub fn NewApp(comptime ssl: bool) type { if (comptime is_bindgen) { unreachable; } - uws_app_any(ssl_flag, @as(*uws_app_t, @ptrCast(app)), pattern, RouteHandler(UserDataType, handler).handle, user_data); + uws_app_any(ssl_flag, @as(*uws_app_t, @ptrCast(app)), pattern.ptr, pattern.len, RouteHandler(UserDataType, handler).handle, user_data); } pub fn domain(app: *ThisApp, pattern: [:0]const u8) void { uws_app_domain(ssl_flag, @as(*uws_app_t, @ptrCast(app)), pattern); @@ -2233,6 +2380,10 @@ pub fn NewApp(comptime ssl: bool) type { return uws_res_state(ssl_flag, @as(*const uws_res, @ptrCast(@alignCast(res)))); } + pub fn shouldCloseConnection(this: *const Response) bool { + return this.state().isHttpConnectionClose(); + } + pub fn prepareForSendfile(res: *Response) void { return uws_res_prepare_for_sendfile(ssl_flag, res.downcast()); } @@ -2324,7 +2475,7 @@ pub fn NewApp(comptime ssl: bool) type { pub fn onWritable( res: *Response, comptime UserDataType: type, - comptime handler: fn (UserDataType, u64, *Response) callconv(.C) bool, + comptime handler: fn (UserDataType, u64, *Response) bool, user_data: UserDataType, ) void { const Wrapper = struct { @@ -2407,22 +2558,19 @@ pub fn NewApp(comptime ssl: bool) type { pub fn corked( res: *Response, - comptime Function: anytype, - args: anytype, - ) @typeInfo(@TypeOf(Function)).Fn.return_type.? { + comptime handler: anytype, + args_tuple: anytype, + ) void { const Wrapper = struct { - opts: @TypeOf(args), - result: @typeInfo(@TypeOf(Function)).Fn.return_type.? = undefined, - pub fn run(this: *@This()) void { - this.result = Function(this.opts); + const handler_fn = handler; + const Args = *@TypeOf(args_tuple); + pub fn handle(user_data: ?*anyopaque) callconv(.C) void { + const args: Args = @alignCast(@ptrCast(user_data.?)); + @call(.always_inline, handler_fn, args.*); } }; - var wrapped = Wrapper{ - .opts = args, - .result = undefined, - }; - runCorkedWithType(res, *Wrapper, Wrapper.run, &wrapped); - return wrapped.result; + + uws_res_cork(ssl_flag, res.downcast(), @constCast(@ptrCast(&args_tuple)), Wrapper.handle); } pub fn runCorkedWithType( @@ -2618,10 +2766,10 @@ extern fn uws_app_options(ssl: i32, app: *uws_app_t, pattern: [*c]const u8, hand extern fn uws_app_delete(ssl: i32, app: *uws_app_t, pattern: [*c]const u8, handler: uws_method_handler, user_data: ?*anyopaque) void; extern fn uws_app_patch(ssl: i32, app: *uws_app_t, pattern: [*c]const u8, handler: uws_method_handler, user_data: ?*anyopaque) void; extern fn uws_app_put(ssl: i32, app: *uws_app_t, pattern: [*c]const u8, handler: uws_method_handler, user_data: ?*anyopaque) void; -extern fn uws_app_head(ssl: i32, app: *uws_app_t, pattern: [*c]const u8, handler: uws_method_handler, user_data: ?*anyopaque) void; +extern fn uws_app_head(ssl: i32, app: *uws_app_t, pattern: [*]const u8, pattern_len: usize, handler: uws_method_handler, user_data: ?*anyopaque) void; extern fn uws_app_connect(ssl: i32, app: *uws_app_t, pattern: [*c]const u8, handler: uws_method_handler, user_data: ?*anyopaque) void; extern fn uws_app_trace(ssl: i32, app: *uws_app_t, pattern: [*c]const u8, handler: uws_method_handler, user_data: ?*anyopaque) void; -extern fn uws_app_any(ssl: i32, app: *uws_app_t, pattern: [*c]const u8, handler: uws_method_handler, user_data: ?*anyopaque) void; +extern fn uws_app_any(ssl: i32, app: *uws_app_t, pattern: [*]const u8, pattern_len: usize, handler: uws_method_handler, user_data: ?*anyopaque) void; extern fn uws_app_run(ssl: i32, *uws_app_t) void; extern fn uws_app_domain(ssl: i32, app: *uws_app_t, domain: [*c]const u8) void; extern fn uws_app_listen(ssl: i32, app: *uws_app_t, port: i32, handler: uws_listen_handler, user_data: ?*anyopaque) void;