diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 5b97a155cfc53..bed018c9a5043 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -2660,7 +2660,7 @@ declare module "bun" { * @param closeActiveConnections Immediately terminate in-flight requests, websockets, and stop accepting new connections. * @default false */ - stop(closeActiveConnections?: boolean): void; + stop(closeActiveConnections?: boolean): Promise; /** * Update the `fetch` and `error` handlers without restarting the server. diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 076fe894b527c..c429032cb558e 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -6300,6 +6300,8 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp } 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| { @@ -6315,7 +6317,7 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp this.stop(abrupt); } - return .undefined; + return rc; } pub fn disposeFromJS(this: *ThisServer) JSC.JSValue { @@ -6508,6 +6510,18 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp 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 { httplog("deinitIfWeCan", .{}); @@ -6522,10 +6536,12 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp const task = ServerAllConnectionsClosedTask.new(.{ .globalObject = this.globalThis, - .promise = this.all_closed_promise, + // Duplicate the Strong handle so that we can hold two independent strong references to it. + .promise = JSC.JSPromise.Strong{ + .strong = JSC.Strong.create(this.all_closed_promise.value(), this.globalThis), + }, .tracker = JSC.AsyncTaskTracker.init(vm), }); - this.all_closed_promise = .{}; event_loop.enqueueTask(JSC.Task.init(task)); } if (this.pending_requests == 0 and this.listener == null and this.flags.has_js_deinited and !this.hasActiveWebSockets()) { @@ -6588,6 +6604,7 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp httplog("deinit", .{}); this.cached_hostname.deref(); this.cached_protocol.deref(); + this.all_closed_promise.deinit(); this.config.deinit(); this.app.destroy(); @@ -7165,12 +7182,11 @@ pub const ServerAllConnectionsClosedTask = struct { defer tracker.didDispatch(globalObject); var promise = this.promise; + defer promise.deinit(); this.destroy(); if (!vm.isShuttingDown()) { promise.resolve(globalObject, .undefined); - } else { - promise.deinit(); } } }; diff --git a/src/bun.js/node/node_http_binding.zig b/src/bun.js/node/node_http_binding.zig index 7ad2be8088e53..a393736cd2c05 100644 --- a/src/bun.js/node/node_http_binding.zig +++ b/src/bun.js/node/node_http_binding.zig @@ -23,15 +23,7 @@ pub fn getBunServerAllClosedPromise(globalThis: *JSC.JSGlobalObject, callframe: JSC.API.DebugHTTPSServer, }) |Server| { if (value.as(Server)) |server| { - if (server.listener == null and server.pending_requests == 0) { - return JSC.JSPromise.resolvedPromise(globalThis, .undefined).asValue(globalThis); - } - const prom = &server.all_closed_promise; - if (prom.strong.has()) { - return prom.value(); - } - prom.* = JSC.JSPromise.Strong.init(globalThis); - return prom.value(); + return server.getAllClosedPromise(globalThis); } } diff --git a/test/js/bun/http/bun-server.test.ts b/test/js/bun/http/bun-server.test.ts index 638f141214c85..f6ebd8a57ac3b 100644 --- a/test/js/bun/http/bun-server.test.ts +++ b/test/js/bun/http/bun-server.test.ts @@ -514,6 +514,78 @@ test("Bun should be able to handle utf16 inside Content-Type header #11316", asy expect(result.headers.get("Content-Type")).toBe("text/html"); }); +test("should be able to await server.stop()", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ready = Promise.withResolvers(); + const received = Promise.withResolvers(); + using server = Bun.serve({ + port: 0, + // Avoid waiting for DNS resolution in fetch() + hostname: "127.0.0.1", + async fetch(req) { + received.resolve(); + await ready.promise; + return new Response("Hello World", { + headers: { + // Prevent Keep-Alive from keeping the connection open + "Connection": "close", + }, + }); + }, + }); + + // Start the request + const responsePromise = fetch(server.url); + // Wait for the server to receive it. + await received.promise; + // Stop listening for new connections + const stopped = server.stop(); + // Continue the request + ready.resolve(); + // Wait for the response + await (await responsePromise).text(); + // Wait for the server to stop + await stopped; + // Ensure the server is completely stopped + expect(async () => await fetch(server.url)).toThrow(); +}); + +test("should be able to await server.stop(true) with keep alive", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ready = Promise.withResolvers(); + const received = Promise.withResolvers(); + using server = Bun.serve({ + port: 0, + // Avoid waiting for DNS resolution in fetch() + hostname: "127.0.0.1", + async fetch(req) { + received.resolve(); + await ready.promise; + return new Response("Hello World"); + }, + }); + + // Start the request + const responsePromise = fetch(server.url); + // Wait for the server to receive it. + await received.promise; + // Stop listening for new connections + const stopped = server.stop(true); + // Continue the request + ready.resolve(); + + // Wait for the server to stop + await stopped; + + // It should fail before the server responds + expect(async () => { + await (await responsePromise).text(); + }).toThrow(); + + // Ensure the server is completely stopped + expect(async () => await fetch(server.url)).toThrow(); +}); + test("should be able to async upgrade using custom protocol", async () => { const { promise, resolve } = Promise.withResolvers<{ code: number; reason: string } | boolean>(); using server = Bun.serve({