node:net: keep the process alive while an early-unref'd/paused socket is still connecting
Fixes #37086
Repro
import net from "node:net";
const server = net.createServer(() => {});
server.listen(0, "127.0.0.1", () => {
const s = new net.Socket();
s.unref(); // before connect()
s.connect(server.address().port, "127.0.0.1", () => {
console.log("CONNECTED");
s.destroy();
server.close();
});
});
server.unref();
Node and Bun 1.3.14 print CONNECTED; Bun 1.4.0 exits 0 silently before the connection completes. This is what makes every testcontainers program exit during container startup (its Ryuk client calls socket.unref() before .connect()). The same happens for s.connect(); s.unref() and s.connect(); s.pause().
Cause
In Node a pending uv_connect_t keeps the loop alive no matter what the handle's ref state is; unref()/readStop only matter once the socket is established. Bun's native socket instead had a ref_pollref_on_connect flag that made an early unref() skip taking the loop hold for the connect attempt altogether, and an unref() on a connecting handle dropped it immediately. #36619 started applying node:net's recorded unref() to the freshly created (detached) handle, which routed the common "unref before connect" case into that path.
Fix
Native (Listener.rs / socket_body.rs): the connect attempt always holds poll_ref (taken before do_connect, released by every failure path); ref()/unref() on a not-yet-established socket only record the preference and on_open applies it. pause()/resume() on a half-connected socket are likewise left for after the open (uws_sys/socket.rs) — usockets re-arms reads on open without clearing its paused bit, so a pause latched there made every later pause() a no-op and a paused stream buffered without bound once connected.
net.ts: an autoSelectFamily retry handle inherits a mid-connect pause() the same way it inherits unref(), and the read(0) that connect() queued on the next tick is left to afterConnect while still connecting, so a pause() issued mid-connect is not undone by a read queued before it (Node only issues that read from afterConnect).
Tests
test/js/node/net/node-net.test.ts:
unref()/pause() around connect(): unref/pause beforeconnect(), right after it, and while the native connect is actually in flight →connectedthen exit;ref()afterunref()(both timings) keeps holding; an in-flightpause()still lets backpressure stop reads. The unref/pause cases print nothing (or hang) on 1.4.0; all match Node's output.unref survives an autoSelectFamily retryno longer needs its 500 ms sentinel timer (the pending connect holds the loop by itself — that sentinel was also why it failed under slow debug/ASAN builds) and gains apause()variant, which hung before this change.
Also ran node-net-server, node-tls-connect, node-tls-server, node-http, socket-reconnect-live, double-connect, bun/net/socket.test.ts and the node parallel net pause/connect tests locally with no new failures.