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
11 changes: 11 additions & 0 deletions src/runtime/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1853,11 +1853,22 @@ impl<const SSL: bool, const DEBUG: bool> NewServer<SSL, DEBUG> {
// config / on_clienterror / h3_alt_svc / dev_server / plugins) is
// handled by the heap::take drop below — see `impl Drop for NewServer`.
if Self::HAS_H3 {
if let Some(h3l) = this_ref.h3_listener.take() {
// S008: `h3::ListenSocket` is an `opaque_ffi!` ZST — safe deref.
bun_opaque::opaque_deref_mut(h3l).close();
}
if let Some(h3a) = this_ref.h3_app.take() {
// SAFETY: live H3::App handle owned by this server.
unsafe { uws_sys::h3::App::destroy(h3a) };
}
}
// listen() may reach here after TCP bind succeeded but H3 bind
// failed; close the TCP listener so `us_socket_group_deinit` (via
// `~TemplatedApp` below) sees an empty `head_listen_sockets`.
if let Some(listener) = this_ref.listener.take() {
// S012: `app::ListenSocket<SSL>` is a ZST opaque — safe deref.
bun_opaque::opaque_deref_mut(listener).close();
}
if let Some(app) = this_ref.app.take() {
// SAFETY: live uws App handle owned by this server.
unsafe { uws_sys::NewApp::<SSL>::destroy(app) };
Expand Down
61 changes: 61 additions & 0 deletions test/js/bun/http/serve-http3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1077,3 +1077,64 @@ describe("Bun.serve HTTP/3 production", () => {
// (HttpContext.h / Http3Context.h call writeContinue before routing); a
// curl --expect100-timeout assertion was flaky enough to drop here.
});

test("Bun.serve http3 UDP bind failure releases the TCP listen socket", async () => {
// When the TCP bind succeeds but the HTTP/3 UDP bind on the same port
// fails, Bun.serve must close the TCP listen socket before tearing down
// the uws App. Otherwise the fd leaks (observable as EADDRINUSE below)
// and debug builds abort in us_socket_group_deinit() on a non-empty
// head_listen_sockets list.
const script = `
const net = require("net");
const dgram = require("dgram");
const tcp = net.createServer();
await new Promise(r => tcp.listen(0, "127.0.0.1", r));
const port = tcp.address().port;
const udp = dgram.createSocket({ type: "udp4", reuseAddr: false });
await new Promise((res, rej) => { udp.once("error", rej); udp.bind(port, "127.0.0.1", res); });
await new Promise(r => tcp.close(r));
let threw = false;
try {
const s = Bun.serve({
port,
hostname: "127.0.0.1",
tls: ${JSON.stringify(tls)},
http3: true,
fetch: () => new Response("x"),
});
s.stop(true);
} catch (e) {
threw = true;
console.log("THREW:" + e.message);
}
udp.close();
if (!threw) {
console.log("NOTHROW");
} else {
const tcp2 = net.createServer();
try {
await new Promise((res, rej) => {
tcp2.once("error", rej);
tcp2.listen(port, "127.0.0.1", res);
});
console.log("REBIND:OK");
tcp2.close();
} catch (e) {
console.log("REBIND:" + e.code);
}
}
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", script],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout: stdout.trim().split("\n"), stderr, signalCode: proc.signalCode }).toEqual({
stdout: [expect.stringContaining("THREW:Failed to listen on UDP port"), "REBIND:OK"],
stderr: expect.not.stringContaining("Assertion"),
signalCode: null,
});
expect(exitCode).toBe(0);
});
Loading