Skip to content
Open
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
14 changes: 5 additions & 9 deletions src/js/node/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3573,6 +3573,8 @@ Server.prototype.close = function close(callback) {
if (this._handle) {
if (typeof this._handle.stop === "function") {
this._handle.stop(false);
// Node's uv_close() keeps the loop alive for one more turn (test-process-beforeexit); so does this.
setImmediate(noop);
// Released here, not on 'close': https://github.com/nodejs/node/blob/v26.3.0/lib/net.js#L2434-L2437
const clusterHandle = this[kClusterHandle];
if (clusterHandle) {
Expand Down Expand Up @@ -3961,14 +3963,8 @@ Server.prototype[kRealListen] = function (
// Unref the handle if the server was unref'ed prior to listening
if (this._unref) this.unref();

// We must schedule the emitListeningNextTick() only after the next run of
// the event loop's IO queue. Otherwise, the server may not actually be listening
// when the 'listening' event is emitted.
//
// That leads to all sorts of confusion.
//
// process.nextTick() is not sufficient because it will run before the IO queue.
setTimeout(emitListeningNextTick, 1, this);
// A tick, not a timer, so a close() from 'listening' runs before the loop accepts anything (as in Node).
process.nextTick(emitListeningNextTick, this);
};

Server.prototype[EventEmitter.captureRejectionSymbol] = function (err, event, sock) {
Expand Down Expand Up @@ -4177,7 +4173,7 @@ Server.prototype[kClusterFauxListen] = function (handle, backlog, path) {
handle[kClusterOwner] = this;
handle.listen(backlog || 511);
if (this._unref) this.unref();
setTimeout(emitListeningNextTick, 1, this);
process.nextTick(emitListeningNextTick, this);
};

function onClusterConnection(err, clientHandle) {
Expand Down
82 changes: 82 additions & 0 deletions test/js/node/net/node-net-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,88 @@ describe("net.createServer listen", () => {
}),
);
});

it("emits 'listening' on the next tick, before the event loop polls", async () => {
const server: Server = createServer();
const order: string[] = [];
server.on("listening", () => order.push("listening"));
server.listen(0);
process.nextTick(() => order.push("nextTick"));
await once(server, "listening");
server.close();
await once(server, "close");
expect(order).toEqual(["listening", "nextTick"]);
});

// How vite, get-port and friends probe for a free port: listen, then close()
// from the 'listening' handler. A peer that connects in between must be reset
// by the kernel when the listening fd closes, not accepted into the closing
// server, whose close() would then wait on a connection nobody is reading.
it("close() from 'listening' does not accept a peer that connected in between", async () => {
const server: Server = createServer();
let accepted = 0;
server.on("connection", () => accepted++);
const { promise: closed, resolve: onClosed, reject } = Promise.withResolvers<void>();
server.on("error", reject);
server.listen(0, "127.0.0.1");

// Bun.connect() issues connect(2) synchronously, so the peer is already
// sitting in the listen backlog when the 'listening' handler runs.
const { port } = server.address() as AddressInfo;
const peer = Bun.connect({
hostname: "127.0.0.1",
port,
socket: {
data() {},
error() {},
connectError() {},
},
}).catch(() => null);

server.once("listening", () => {
server.close(() => onClosed());
peer.then(socket => socket?.end());
});

await closed;
expect(accepted).toBe(0);
});

// Node's server.close() completes the handle's uv_close() on the next loop
// turn, so a server listened and closed from a 'beforeExit' handler brings
// the loop back to life once more and 'beforeExit' fires again
// (upstream test-process-beforeexit). 'listening' itself is only a nextTick
// now, so close() has to hold the loop for that turn on its own.
it("closing a server listened from 'beforeExit' re-emits 'beforeExit'", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const net = require("net");
process.once("beforeExit", () => {
net
.createServer()
.listen(0)
.on("listening", function () {
this.close();
process.once("beforeExit", () => console.log("beforeExit again"));
});
});
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// stderr only matters on failure: debug builds may print benign warnings.
expect({ stdout, exitCode, failureDetail: exitCode === 0 ? "" : stderr }).toEqual({
stdout: "beforeExit again\n",
exitCode: 0,
failureDetail: "",
});
});
});

describe("net.createServer events", () => {
Expand Down
Loading