Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/bun-uws/src/App.h
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,18 @@ struct TemplatedApp {
return std::move(*this);
}

/** Closes all connections (idle and in-flight) connected to this server. Does not close the listen socket. */
TemplatedApp &&closeAllConnections() {
auto *group = httpContext->getSocketGroup();
struct us_socket_t *s = group->head_sockets;
while (s) {
struct us_socket_t *next = s->next;
us_socket_close(s, LIBUS_SOCKET_CLOSE_CODE_CLEAN_SHUTDOWN, 0);
s = next;
}
return std::move(*this);
}

template <typename UserData>
TemplatedApp &&ws(std::string_view pattern, WebSocketBehavior<UserData> &&behavior) {
/* Don't compile if alignment rules cannot be satisfied */
Expand Down
12 changes: 4 additions & 8 deletions src/js/node/_http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,14 +310,10 @@ Server.prototype.closeAllConnections = function () {
if (!server) {
return;
}
this[serverSymbol] = undefined;
const connectionsCheckingInterval = this[kConnectionsCheckingInterval];
if (connectionsCheckingInterval) {
connectionsCheckingInterval._destroyed = true;
}
this.listening = false;

server.stop(true);
// Per Node, this only closes established HTTP(S) connections (idle and
// in-flight). The listening socket must stay open so subsequent requests
// can still be accepted — see https://nodejs.org/api/http.html#serverclosealllconnections.
server.closeAllConnections?.();
};

Server.prototype.closeIdleConnections = function () {
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/server/server.classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ function generate(name) {
fn: "closeIdleConnections",
length: 0,
},
closeAllConnections: {
fn: "closeAllConnections",
length: 0,
},
stop: {
fn: "doStop",
length: 1,
Expand Down
13 changes: 13 additions & 0 deletions src/runtime/server/server_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2521,6 +2521,19 @@ where
Ok(JSValue::UNDEFINED)
}

#[bun_jsc::host_fn(method)]
pub fn close_all_connections(
&mut self,
_global: &JSGlobalObject,
_callframe: &CallFrame,
) -> JsResult<JSValue> {
if self.app.is_none() {
return Ok(JSValue::UNDEFINED);
}
self.app_mut().close_all_connections();
Ok(JSValue::UNDEFINED)
}

pub fn stop_from_js(&mut self, abruptly: Option<JSValue>) -> JSValue {
let rc = self.get_all_closed_promise(&self.global());

Expand Down
7 changes: 7 additions & 0 deletions src/uws_sys/App.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ impl<const SSL: bool> App<SSL> {
c::uws_app_close_idle(Self::SSL_FLAG, self.as_raw())
}

/// Closes all client connections (idle and in-flight) without touching
/// the listening socket. Equivalent to Node's `Server#closeAllConnections`.
pub fn close_all_connections(&mut self) {
c::uws_app_close_all_connections(Self::SSL_FLAG, self.as_raw())
}

pub fn create(opts: &BunSocketContextOptions) -> Option<*mut Self> {
// SAFETY: FFI call; uws_create_app returns null on failure.
let app = unsafe { c::uws_create_app(Self::SSL_FLAG, *opts) };
Expand Down Expand Up @@ -516,6 +522,7 @@ pub mod c {
unsafe extern "C" {
pub(crate) safe fn uws_app_close(ssl: i32, app: &mut uws_app_s);
pub(crate) safe fn uws_app_close_idle(ssl: i32, app: &mut uws_app_s);
pub(crate) safe fn uws_app_close_all_connections(ssl: i32, app: &mut uws_app_s);
// safe: `&mut uws_app_s` is ABI-identical to a non-null `*mut`;
// `handler`/`user_data` are stored opaquely (never dereferenced by the
// C++ shim itself) — no preconditions on this call.
Expand Down
14 changes: 14 additions & 0 deletions src/uws_sys/libuwsockets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,20 @@ extern "C"
}
}

void uws_app_close_all_connections(int ssl, uws_app_t *app)
{
if (ssl)
{
uWS::SSLApp *uwsApp = (uWS::SSLApp *)app;
uwsApp->closeAllConnections();
}
else
{
uWS::App *uwsApp = (uWS::App *)app;
uwsApp->closeAllConnections();
}
}

void uws_app_set_on_clienterror(int ssl, uws_app_t *app, void (*handler)(void *user_data, int is_ssl, struct us_socket_t *rawSocket, uint8_t errorCode, char *rawPacket, int rawPacketLength), void *user_data)
{
if (ssl)
Expand Down
69 changes: 69 additions & 0 deletions test/js/node/http/node-http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1369,6 +1369,75 @@ it.skip("should be able to stream huge amounts of data", async () => {
}
}, 30_000);

describe("Server.closeAllConnections", () => {
// Per https://nodejs.org/api/http.html#serverclosealllconnections this should
// forcefully close every established connection but leave the listener open.
it("closes established connections but keeps the listener accepting new ones", async () => {
const handlerCalls: number[] = [];
const server = http.createServer((req, res) => {
handlerCalls.push(1);
res.end("ok");
});
try {
server.listen(0);
await once(server, "listening");
const port = (server.address() as AddressInfo).port;

// Open a keep-alive connection and finish one request so the socket
// is established and idle.
const keepAlive = connect(port);
await once(keepAlive, "connect");
const firstResponse = new Promise<string>(resolve => {
let buf = "";
keepAlive.setEncoding("utf8");
keepAlive.on("data", chunk => {
buf += chunk;
if (buf.endsWith("ok")) resolve(buf);
});
});
keepAlive.write("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n");
await firstResponse;

// Sanity: the socket is still open from the client's view.
expect(keepAlive.destroyed).toBe(false);
expect(server.listening).toBe(true);

const closed = once(keepAlive, "close");
server.closeAllConnections();
await closed;
expect(keepAlive.destroyed).toBe(true);

// Listener must still accept new connections after closeAllConnections.
expect(server.listening).toBe(true);
const followup = await fetch(`http://localhost:${port}/`);
expect(followup.status).toBe(200);
expect(await followup.text()).toBe("ok");
expect(handlerCalls.length).toBe(2);
} finally {
server.close();
}
});

it("is a no-op when no connections are established", async () => {
const server = http.createServer((req, res) => res.end("ok"));
try {
server.listen(0);
await once(server, "listening");
const port = (server.address() as AddressInfo).port;

// No client has connected yet — must not throw and must not affect the listener.
expect(() => server.closeAllConnections()).not.toThrow();
expect(server.listening).toBe(true);

const res = await fetch(`http://localhost:${port}/`);
expect(res.status).toBe(200);
expect(await res.text()).toBe("ok");
} finally {
server.close();
}
});
});

describe("HTTP Server Security Tests - Advanced", () => {
// Setup and teardown utilities
let server;
Expand Down