Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
72 changes: 72 additions & 0 deletions doc/api/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -2084,6 +2084,77 @@ net.isIPv6('::1'); // returns true
net.isIPv6('fhqwhgads'); // returns false
```

## `net/promises` API

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

The `net/promises` API provides a set of `net` functions that return `Promise`
objects rather than relying on events. The API is accessible via
`require('node:net').promises` or `require('node:net/promises')`.

### `netPromises.connect(options)`

### `netPromises.connect(path)`

### `netPromises.connect(port[, host])`

<!-- YAML
added: REPLACEME
-->

* `options` {Object} Accepts the same arguments as [`net.connect()`][]. May
include a `signal` {AbortSignal} that can be used to abort an in-progress
connection attempt.
* Returns: {Promise} Fulfills with a connected [`net.Socket`][].

A promise-based alternative to [`net.connect()`][]. The returned promise is
fulfilled with the socket once its [`'connect'`][] event fires, and is rejected
if the connection fails or the `signal` is aborted. When the promise rejects,
the underlying socket is destroyed.

This API is named for the action it performs and awaits — connecting — to
parallel [`netPromises.listen()`][]. It is not named `createConnection()`,
because that name belongs to the socket-factory taxonomy of the callback API,
which has no counterpart here.

```mjs
import { connect } from 'node:net/promises';

const socket = await connect({ port: 8124 });
socket.write('hello world!');
socket.end();
```

### `netPromises.listen([options])`

<!-- YAML
added: REPLACEME
-->

* `options` {Object} Accepts the same options as [`net.createServer()`][] and
[`server.listen()`][], plus:
* `connectionListener` {Function} Automatically set as a listener for the
[`'connection'`][] event.
* `signal` {AbortSignal} An `AbortSignal` that may be used to abort the
listening server.
* Returns: {Promise} Fulfills with a listening [`net.Server`][].

Creates a [`net.Server`][] and begins listening. The returned promise is
fulfilled with the server once its [`'listening'`][] event fires, and is
rejected if the server fails to bind or the `signal` is aborted. When the
promise rejects, the server is closed.

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

const server = await listen({ port: 8124 });
console.log('server bound to', server.address().port);
```

[IPC]: #ipc-support
[Identifying paths for IPC connections]: #identifying-paths-for-ipc-connections
[RFC 8305]: https://www.rfc-editor.org/rfc/rfc8305.txt
Expand Down Expand Up @@ -2114,6 +2185,7 @@ net.isIPv6('fhqwhgads'); // returns false
[`net.createServer()`]: #netcreateserveroptions-connectionlistener
[`net.getDefaultAutoSelectFamily()`]: #netgetdefaultautoselectfamily
[`net.getDefaultAutoSelectFamilyAttemptTimeout()`]: #netgetdefaultautoselectfamilyattempttimeout
[`netPromises.listen()`]: #netpromiseslistenoptions
[`new net.Socket(options)`]: #new-netsocketoptions
[`readable.setEncoding()`]: stream.md#readablesetencodingencoding
[`server.close()`]: #serverclosecallback
Expand Down
81 changes: 81 additions & 0 deletions lib/internal/net/promises.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use strict';

const { once } = require('events');
const {
validateAbortSignal,
validateObject,
} = require('internal/validators');
const { AbortError } = require('internal/errors');
const { kEmptyObject } = require('internal/util');

// Lazily loaded to avoid a require cycle with the `net` module, which exposes
// this namespace through its `promises` getter.
let net;
function lazyNet() {
net ??= require('net');
return net;
}

// Resolves with a connected `net.Socket` once the `'connect'` event fires, and
// rejects if the connection fails or the optional `signal` is aborted.
async function connect(...args) {
const lazy = lazyNet();
const options = lazy._normalizeArgs(args)[0];
const { signal } = options;
if (signal !== undefined) {
validateAbortSignal(signal, 'options.signal');
if (signal.aborted) {
Comment thread
Ethan-Arrowood marked this conversation as resolved.
Outdated
throw new AbortError(undefined, { cause: signal.reason });
}
}

// Strip the signal so the socket does not also install its own abort
// handling; rejecting and destroying below fully tears the socket down.
const socket = lazy.connect({ ...options, signal: undefined });

try {
await once(socket, 'connect', signal !== undefined ? { signal } : kEmptyObject);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the socket errors before the connect event, would this end up hanging?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I believe this correctly rejects. The ECONNREFUSED test in test-net-promises-connect.js should be covering this (https://github.com/nodejs/node/pull/63965/changes#diff-20589a91d7b722a33615840a6188115a9a297c2942b6d49ce1615e6dd8dfec71R35)

} catch (err) {
socket.destroy();
throw err;
}
return socket;
}

// Creates a server and resolves with it once it is listening, rejecting if it
// fails to bind or the optional `signal` is aborted.
async function listen(options = kEmptyObject) {
validateObject(options, 'options');
const { signal, connectionListener } = options;
if (signal !== undefined) {
validateAbortSignal(signal, 'options.signal');
if (signal.aborted) {
Comment thread
Ethan-Arrowood marked this conversation as resolved.
Outdated
throw new AbortError(undefined, { cause: signal.reason });
}
}

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

try {
server.listen(serverOptions);
await once(server, 'listening', signal !== undefined ? { signal } : kEmptyObject);
} catch (err) {
server.close();
throw err;
}
return server;
}

module.exports = {
connect,
listen,
get isIP() { return lazyNet().isIP; },
get isIPv4() { return lazyNet().isIPv4; },
get isIPv6() { return lazyNet().isIPv6; },
get BlockList() { return lazyNet().BlockList; },
get SocketAddress() { return lazyNet().SocketAddress; },
};
5 changes: 5 additions & 0 deletions lib/net.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ let cluster;
let dns;
let BlockList;
let SocketAddress;
let netPromises;
let autoSelectFamilyDefault = getOptionValue('--network-family-autoselection');
let autoSelectFamilyAttemptTimeoutDefault = getOptionValue('--network-family-autoselection-attempt-timeout');

Expand Down Expand Up @@ -2576,6 +2577,10 @@ module.exports = {
connect,
createConnection: connect,
createServer,
get promises() {
netPromises ??= require('internal/net/promises');
return netPromises;
},
isIP: isIP,
isIPv4: isIPv4,
isIPv6: isIPv6,
Expand Down
3 changes: 3 additions & 0 deletions lib/net/promises.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports = require('internal/net/promises');
62 changes: 62 additions & 0 deletions test/parallel/test-net-promises-connect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const net = require('net');
const { once } = require('events');
const { connect } = require('net/promises');

(async () => {
// Resolves with a connected socket and round-trips data.
{
const server = net.createServer((socket) => {
socket.end('hello');
}).listen(0);
await once(server, 'listening');
const socket = await connect({ port: server.address().port });
assert.strictEqual(socket.connecting, false);
const chunks = [];
for await (const chunk of socket) {
chunks.push(chunk);
}
assert.strictEqual(Buffer.concat(chunks).toString(), 'hello');
server.close();
}

// net.promises is the same object as require('net/promises').
assert.strictEqual(net.promises, require('net/promises'));

// Rejects when the connection is refused.
{
const server = net.createServer().listen(0);
await once(server, 'listening');
const { port } = server.address();
server.close();
await once(server, 'close');
await assert.rejects(connect({ port }), { code: 'ECONNREFUSED' });
}

// A pre-aborted signal rejects with an AbortError.
{
await assert.rejects(
connect({ port: 0, signal: AbortSignal.abort() }),
{ name: 'AbortError' });
}

// Aborting while connecting rejects with an AbortError.
{
const server = net.createServer().listen(0);
await once(server, 'listening');
const controller = new AbortController();
const promise = connect({ port: server.address().port, signal: controller.signal });
controller.abort();
await assert.rejects(promise, { name: 'AbortError' });
server.close();
}

// An invalid signal throws.
{
await assert.rejects(
connect({ port: 0, signal: 'INVALID_SIGNAL' }),
{ code: 'ERR_INVALID_ARG_TYPE' });
}
})().then(common.mustCall());
58 changes: 58 additions & 0 deletions test/parallel/test-net-promises-listen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const net = require('net');
const { listen } = require('net/promises');

(async () => {
// Resolves with a listening server.
{
const server = await listen({ port: 0 });
assert.strictEqual(server.listening, true);
assert.strictEqual(typeof server.address().port, 'number');
server.close();
}

// The connectionListener option receives incoming connections.
{
const server = await listen({
port: 0,
connectionListener: common.mustCall((socket) => {
socket.end();
server.close();
}),
});
const client = net.connect(server.address().port);
client.resume();
}

// A pre-aborted signal rejects with an AbortError.
{
await assert.rejects(
listen({ port: 0, signal: AbortSignal.abort() }),
{ name: 'AbortError' });
}

// Aborting while binding rejects with an AbortError and closes the server.
{
const controller = new AbortController();
const promise = listen({ port: 0, signal: controller.signal });
controller.abort();
await assert.rejects(promise, { name: 'AbortError' });
}

// An invalid signal throws.
{
await assert.rejects(
listen({ port: 0, signal: 'INVALID_SIGNAL' }),
{ code: 'ERR_INVALID_ARG_TYPE' });
}

// Rejects when the address is already in use.
{
const first = await listen({ port: 0 });
const { port } = first.address();
await assert.rejects(listen({ port }), { code: 'EADDRINUSE' });
first.close();
}
})().then(common.mustCall());
Loading