Skip to content

Commit 84ae422

Browse files
net: tie listen() signal to server lifetime, async-iterable Server
Address review feedback on the net/promises API: - `listen()` now passes its `signal` through to `server.listen()` so the signal aborts the server for its entire lifetime rather than only the pending listen. Aborting before the server is listening rejects the returned promise; aborting afterwards closes the server, matching net.Server's own `signal` behavior. - `net.Server` is now async iterable: `for await (const socket of server)` yields each incoming connection as an alternative to the 'connection' event, mirroring readline.Interface. Also update test-repl-tab-complete-require and test-repl-tab-complete-import to account for the new `net/promises` builtin appearing in completions. Refs: #21482 Assisted-by: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Ethan Arrowood <ethan@arrowood.dev>
1 parent ab5b8e9 commit 84ae422

7 files changed

Lines changed: 132 additions & 16 deletions

File tree

‎doc/api/net.md‎

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,35 @@ changes:
452452
Calls [`server.close()`][] and returns a promise that fulfills when the
453453
server has closed.
454454

455+
### `server[Symbol.asyncIterator]()`
456+
457+
<!-- YAML
458+
added: REPLACEME
459+
-->
460+
461+
> Stability: 1 - Experimental
462+
463+
* Returns: {AsyncIterator} An async iterator that yields each incoming
464+
[`net.Socket`][].
465+
466+
Returns an async iterator over the server's incoming connections, allowing them
467+
to be consumed with `for await...of` as an alternative to the [`'connection'`][]
468+
event. Iteration ends when the server emits [`'close'`][], and rejects if the
469+
server emits [`'error'`][].
470+
471+
```mjs
472+
import { createServer } from 'node:net';
473+
474+
const server = createServer().listen(8124);
475+
for await (const socket of server) {
476+
socket.end('hello world!');
477+
}
478+
```
479+
480+
Connections are buffered while the loop body is busy; the server does not stop
481+
accepting them, so a consumer that is slower than the connection rate can buffer
482+
without bound. Use [`server.maxConnections`][] to bound concurrency.
483+
455484
### `server.getConnections(callback)`
456485

457486
<!-- YAML
@@ -2297,19 +2326,26 @@ added: REPLACEME
22972326
* `connectionListener` {Function} Automatically set as a listener for the
22982327
[`'connection'`][] event.
22992328
* `signal` {AbortSignal} An `AbortSignal` that may be used to abort the
2300-
listening server.
2329+
server. Aborting before the server is listening rejects the returned
2330+
promise with an `AbortError`; aborting at any later point closes the
2331+
server, matching the `signal` option of [`server.listen()`][].
23012332
* Returns: {Promise} Fulfills with a listening [`net.Server`][].
23022333

23032334
Creates a [`net.Server`][] and begins listening. The returned promise is
23042335
fulfilled with the server once its [`'listening'`][] event fires, and is
2305-
rejected if the server fails to bind or the `signal` is aborted. When the
2306-
promise rejects, the server is closed.
2336+
rejected if the server fails to bind or the `signal` is aborted before it is
2337+
listening. When the promise rejects, the server is closed.
2338+
2339+
The resolved server is async iterable, so incoming connections can be consumed
2340+
with `for await...of` (see `server[Symbol.asyncIterator]()`).
23072341

23082342
```mjs
23092343
import { listen } from 'node:net/promises';
23102344

23112345
const server = await listen({ port: 8124 });
2312-
console.log('server bound to', server.address().port);
2346+
for await (const socket of server) {
2347+
socket.end('hello world!');
2348+
}
23132349
```
23142350

23152351
[IPC]: #ipc-support

‎lib/internal/net/promises.js‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,21 @@ async function listen(options = kEmptyObject) {
5555
}
5656

5757
const lazy = lazyNet();
58-
// Strip the signal so listen() does not install its own close-on-abort
59-
// handler; rejecting and closing below tears the server down.
60-
const serverOptions = { ...options, signal: undefined };
61-
const server = lazy.createServer(serverOptions, connectionListener);
58+
const server = lazy.createServer(options, connectionListener);
6259

6360
try {
64-
server.listen(serverOptions);
61+
// Pass `signal` through to listen() so net installs its own
62+
// close-on-abort handler: the signal aborts the server for its entire
63+
// lifetime, not just the pending listen.
64+
server.listen(options);
6565
await once(server, 'listening', signal !== undefined ? { signal } : kEmptyObject);
6666
} catch (err) {
67-
server.close();
67+
// On abort, net's signal handler already closes the server, so closing
68+
// again would be redundant; on other failures (e.g. a bind error) there
69+
// is no such handler, so close it here.
70+
if (!signal?.aborted) {
71+
server.close();
72+
}
6873
throw err;
6974
}
7075
return server;

‎lib/net.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const {
3737
ObjectSetPrototypeOf,
3838
Symbol,
3939
SymbolAsyncDispose,
40+
SymbolAsyncIterator,
4041
SymbolDispose,
4142
} = primordials;
4243

@@ -158,6 +159,7 @@ let dns;
158159
let BlockList;
159160
let SocketAddress;
160161
let netPromises;
162+
let kFirstEventParam;
161163
let autoSelectFamilyDefault = getOptionValue('--network-family-autoselection');
162164
let autoSelectFamilyAttemptTimeoutDefault = getOptionValue('--network-family-autoselection-attempt-timeout');
163165

@@ -2763,6 +2765,14 @@ Server.prototype[SymbolAsyncDispose] = async function() {
27632765
await FunctionPrototypeCall(promisify(this.close), this);
27642766
};
27652767

2768+
Server.prototype[SymbolAsyncIterator] = function() {
2769+
kFirstEventParam ??= require('internal/events/symbols').kFirstEventParam;
2770+
return EventEmitter.on(this, 'connection', {
2771+
close: ['close'],
2772+
[kFirstEventParam]: true,
2773+
});
2774+
};
2775+
27662776
Server.prototype._emitCloseIfDrained = function() {
27672777
debug('SERVER _emitCloseIfDrained');
27682778

‎test/parallel/test-net-promises-listen.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const common = require('../common');
33
const assert = require('assert');
44
const net = require('net');
5+
const { once } = require('events');
56
const { listen } = require('net/promises');
67

78
(async () => {
@@ -13,6 +14,18 @@ const { listen } = require('net/promises');
1314
server.close();
1415
}
1516

17+
// The signal aborts the server for its entire lifetime, not just the
18+
// pending listen: aborting after it is listening closes the server.
19+
{
20+
const controller = new AbortController();
21+
const server = await listen({ port: 0, signal: controller.signal });
22+
assert.strictEqual(server.listening, true);
23+
const closed = once(server, 'close');
24+
controller.abort();
25+
await closed;
26+
assert.strictEqual(server.listening, false);
27+
}
28+
1629
// The connectionListener option receives incoming connections.
1730
{
1831
const server = await listen({
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const net = require('net');
5+
const { once } = require('events');
6+
7+
(async () => {
8+
// `for await...of` yields each incoming connection as a net.Socket.
9+
{
10+
const server = net.createServer().listen(0);
11+
await once(server, 'listening');
12+
const { port } = server.address();
13+
14+
const total = 3;
15+
const clients = [];
16+
for (let i = 0; i < total; i++) {
17+
clients.push(net.connect(port));
18+
}
19+
20+
let count = 0;
21+
for await (const socket of server) {
22+
assert.ok(socket instanceof net.Socket);
23+
socket.end();
24+
if (++count === total) break;
25+
}
26+
assert.strictEqual(count, total);
27+
28+
// Breaking out of the loop stops iterating but does not close the server.
29+
assert.strictEqual(server.listening, true);
30+
31+
server.close();
32+
for (const client of clients) {
33+
client.destroy();
34+
}
35+
}
36+
37+
// Iteration ends cleanly when the server closes.
38+
{
39+
const server = net.createServer().listen(0);
40+
await once(server, 'listening');
41+
process.nextTick(() => server.close());
42+
43+
let count = 0;
44+
for await (const socket of server) {
45+
count++;
46+
socket.end();
47+
}
48+
assert.strictEqual(count, 0);
49+
}
50+
})().then(common.mustCall());

‎test/parallel/test-repl-tab-complete-import.js‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,12 @@ replServer.complete("import\t( 'n", common.mustSucceed((data) => {
5252
assert.notStrictEqual(lastIndex, -1);
5353
});
5454
assert.strictEqual(completions[lastIndex + 1], '');
55-
// There is only one Node.js module that starts with n:
55+
// The Node.js modules that start with n:
5656
assert.strictEqual(completions[lastIndex + 2], 'net');
57-
assert.strictEqual(completions[lastIndex + 3], '');
57+
assert.strictEqual(completions[lastIndex + 3], 'net/promises');
58+
assert.strictEqual(completions[lastIndex + 4], '');
5859
// It's possible to pick up non-core modules too
59-
for (const completion of completions.slice(lastIndex + 4)) {
60+
for (const completion of completions.slice(lastIndex + 5)) {
6061
assert.match(completion, /^n/);
6162
}
6263
}));

‎test/parallel/test-repl-tab-complete-require.js‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,12 @@ const { startNewREPLServer } = require('../common/repl');
6767
assert.notStrictEqual(lastIndex, -1);
6868
}
6969
assert.strictEqual(data[0][lastIndex + 1], '');
70-
// There is only one Node.js module that starts with n:
70+
// The Node.js modules that start with n:
7171
assert.strictEqual(data[0][lastIndex + 2], 'net');
72-
assert.strictEqual(data[0][lastIndex + 3], '');
72+
assert.strictEqual(data[0][lastIndex + 3], 'net/promises');
73+
assert.strictEqual(data[0][lastIndex + 4], '');
7374
// It's possible to pick up non-core modules too
74-
for (const completion of data[0].slice(lastIndex + 4)) {
75+
for (const completion of data[0].slice(lastIndex + 5)) {
7576
assert.match(completion, /^n/);
7677
}
7778
})

0 commit comments

Comments
 (0)