summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYoshiya Hinosawa <stibium121@gmail.com>2024-11-12 19:54:47 +0900
committerGitHub <noreply@github.com>2024-11-12 19:54:47 +0900
commitc3c2b379669b17e5fdcbe5e62662404ca22c71c6 (patch)
tree91aae17a3c9dbad8aad35d41d52767b6df4dd24b
parent90236d67c591d4344a9ca0e5d23a4906d08308e5 (diff)
fix(ext/node): add autoSelectFamily option to net.createConnection (#26661)
-rw-r--r--ext/node/polyfills/internal/errors.ts26
-rw-r--r--ext/node/polyfills/internal/net.ts1
-rw-r--r--ext/node/polyfills/internal_binding/uv.ts2
-rw-r--r--ext/node/polyfills/net.ts490
-rw-r--r--tests/node_compat/config.jsonc2
-rw-r--r--tests/node_compat/runner/TODO.md1
-rw-r--r--tests/node_compat/test/common/index.js1
-rw-r--r--tests/node_compat/test/parallel/test-net-autoselectfamily.js312
-rw-r--r--tests/unit_node/http2_test.ts5
9 files changed, 824 insertions, 16 deletions
diff --git a/ext/node/polyfills/internal/errors.ts b/ext/node/polyfills/internal/errors.ts
index 5a3d4437a..962ca86e9 100644
--- a/ext/node/polyfills/internal/errors.ts
+++ b/ext/node/polyfills/internal/errors.ts
@@ -18,7 +18,7 @@
*/
import { primordials } from "ext:core/mod.js";
-const { JSONStringify, SymbolFor } = primordials;
+const { JSONStringify, SafeArrayIterator, SymbolFor } = primordials;
import { format, inspect } from "ext:deno_node/internal/util/inspect.mjs";
import { codes } from "ext:deno_node/internal/error_codes.ts";
import {
@@ -1874,6 +1874,11 @@ export class ERR_SOCKET_CLOSED extends NodeError {
super("ERR_SOCKET_CLOSED", `Socket is closed`);
}
}
+export class ERR_SOCKET_CONNECTION_TIMEOUT extends NodeError {
+ constructor() {
+ super("ERR_SOCKET_CONNECTION_TIMEOUT", `Socket connection timeout`);
+ }
+}
export class ERR_SOCKET_DGRAM_IS_CONNECTED extends NodeError {
constructor() {
super("ERR_SOCKET_DGRAM_IS_CONNECTED", `Already connected`);
@@ -2633,11 +2638,30 @@ export function aggregateTwoErrors(
}
return innerError || outerError;
}
+
+export class NodeAggregateError extends AggregateError {
+ code: string;
+ constructor(errors, message) {
+ super(new SafeArrayIterator(errors), message);
+ this.code = errors[0]?.code;
+ }
+
+ get [kIsNodeError]() {
+ return true;
+ }
+
+ // deno-lint-ignore adjacent-overload-signatures
+ get ["constructor"]() {
+ return AggregateError;
+ }
+}
+
codes.ERR_IPC_CHANNEL_CLOSED = ERR_IPC_CHANNEL_CLOSED;
codes.ERR_INVALID_ARG_TYPE = ERR_INVALID_ARG_TYPE;
codes.ERR_INVALID_ARG_VALUE = ERR_INVALID_ARG_VALUE;
codes.ERR_OUT_OF_RANGE = ERR_OUT_OF_RANGE;
codes.ERR_SOCKET_BAD_PORT = ERR_SOCKET_BAD_PORT;
+codes.ERR_SOCKET_CONNECTION_TIMEOUT = ERR_SOCKET_CONNECTION_TIMEOUT;
codes.ERR_BUFFER_OUT_OF_BOUNDS = ERR_BUFFER_OUT_OF_BOUNDS;
codes.ERR_UNKNOWN_ENCODING = ERR_UNKNOWN_ENCODING;
codes.ERR_PARSE_ARGS_INVALID_OPTION_VALUE = ERR_PARSE_ARGS_INVALID_OPTION_VALUE;
diff --git a/ext/node/polyfills/internal/net.ts b/ext/node/polyfills/internal/net.ts
index 144612626..a3dcb3ed2 100644
--- a/ext/node/polyfills/internal/net.ts
+++ b/ext/node/polyfills/internal/net.ts
@@ -95,4 +95,5 @@ export function makeSyncWrite(fd: number) {
};
}
+export const kReinitializeHandle = Symbol("kReinitializeHandle");
export const normalizedArgsSymbol = Symbol("normalizedArgs");
diff --git a/ext/node/polyfills/internal_binding/uv.ts b/ext/node/polyfills/internal_binding/uv.ts
index aa468a0a5..6cd70a7e8 100644
--- a/ext/node/polyfills/internal_binding/uv.ts
+++ b/ext/node/polyfills/internal_binding/uv.ts
@@ -530,10 +530,12 @@ export function mapSysErrnoToUvErrno(sysErrno: number): number {
export const UV_EAI_MEMORY = codeMap.get("EAI_MEMORY")!;
export const UV_EBADF = codeMap.get("EBADF")!;
+export const UV_ECANCELED = codeMap.get("ECANCELED")!;
export const UV_EEXIST = codeMap.get("EEXIST");
export const UV_EINVAL = codeMap.get("EINVAL")!;
export const UV_ENOENT = codeMap.get("ENOENT");
export const UV_ENOTSOCK = codeMap.get("ENOTSOCK")!;
+export const UV_ETIMEDOUT = codeMap.get("ETIMEDOUT")!;
export const UV_UNKNOWN = codeMap.get("UNKNOWN")!;
export function errname(errno: number): string {
diff --git a/ext/node/polyfills/net.ts b/ext/node/polyfills/net.ts
index 48e1d0de8..2b0112519 100644
--- a/ext/node/polyfills/net.ts
+++ b/ext/node/polyfills/net.ts
@@ -31,6 +31,7 @@ import {
isIP,
isIPv4,
isIPv6,
+ kReinitializeHandle,
normalizedArgsSymbol,
} from "ext:deno_node/internal/net.ts";
import { Duplex } from "node:stream";
@@ -50,9 +51,11 @@ import {
ERR_SERVER_ALREADY_LISTEN,
ERR_SERVER_NOT_RUNNING,
ERR_SOCKET_CLOSED,
+ ERR_SOCKET_CONNECTION_TIMEOUT,
errnoException,
exceptionWithHostPort,
genericNodeError,
+ NodeAggregateError,
uvExceptionWithHostPort,
} from "ext:deno_node/internal/errors.ts";
import type { ErrnoException } from "ext:deno_node/internal/errors.ts";
@@ -80,6 +83,7 @@ import { Buffer } from "node:buffer";
import type { LookupOneOptions } from "ext:deno_node/internal/dns/utils.ts";
import {
validateAbortSignal,
+ validateBoolean,
validateFunction,
validateInt32,
validateNumber,
@@ -100,13 +104,25 @@ import { ShutdownWrap } from "ext:deno_node/internal_binding/stream_wrap.ts";
import { assert } from "ext:deno_node/_util/asserts.ts";
import { isWindows } from "ext:deno_node/_util/os.ts";
import { ADDRCONFIG, lookup as dnsLookup } from "node:dns";
-import { codeMap } from "ext:deno_node/internal_binding/uv.ts";
+import {
+ codeMap,
+ UV_ECANCELED,
+ UV_ETIMEDOUT,
+} from "ext:deno_node/internal_binding/uv.ts";
import { guessHandleType } from "ext:deno_node/internal_binding/util.ts";
import { debuglog } from "ext:deno_node/internal/util/debuglog.ts";
import type { DuplexOptions } from "ext:deno_node/_stream.d.ts";
import type { BufferEncoding } from "ext:deno_node/_global.d.ts";
import type { Abortable } from "ext:deno_node/_events.d.ts";
import { channel } from "node:diagnostics_channel";
+import { primordials } from "ext:core/mod.js";
+
+const {
+ ArrayPrototypeIncludes,
+ ArrayPrototypePush,
+ FunctionPrototypeBind,
+ MathMax,
+} = primordials;
let debug = debuglog("net", (fn) => {
debug = fn;
@@ -120,6 +136,9 @@ const kBytesWritten = Symbol("kBytesWritten");
const DEFAULT_IPV4_ADDR = "0.0.0.0";
const DEFAULT_IPV6_ADDR = "::";
+let autoSelectFamilyDefault = true;
+let autoSelectFamilyAttemptTimeoutDefault = 250;
+
type Handle = TCP | Pipe;
interface HandleOptions {
@@ -214,6 +233,8 @@ interface TcpSocketConnectOptions extends ConnectOptions {
hints?: number;
family?: number;
lookup?: LookupFunction;
+ autoSelectFamily?: boolean | undefined;
+ autoSelectFamilyAttemptTimeout?: number | undefined;
}
interface IpcSocketConnectOptions extends ConnectOptions {
@@ -316,12 +337,6 @@ export function _normalizeArgs(args: unknown[]): NormalizedArgs {
return arr;
}
-function _isTCPConnectWrap(
- req: TCPConnectWrap | PipeConnectWrap,
-): req is TCPConnectWrap {
- return "localAddress" in req && "localPort" in req;
-}
-
function _afterConnect(
status: number,
// deno-lint-ignore no-explicit-any
@@ -372,7 +387,7 @@ function _afterConnect(
socket.connecting = false;
let details;
- if (_isTCPConnectWrap(req)) {
+ if (req.localAddress && req.localPort) {
details = req.localAddress + ":" + req.localPort;
}
@@ -384,7 +399,7 @@ function _afterConnect(
details,
);
- if (_isTCPConnectWrap(req)) {
+ if (details) {
ex.localAddress = req.localAddress;
ex.localPort = req.localPort;
}
@@ -393,6 +408,107 @@ function _afterConnect(
}
}
+function _createConnectionError(req, status) {
+ let details;
+
+ if (req.localAddress && req.localPort) {
+ details = req.localAddress + ":" + req.localPort;
+ }
+
+ const ex = exceptionWithHostPort(
+ status,
+ "connect",
+ req.address,
+ req.port,
+ details,
+ );
+ if (details) {
+ ex.localAddress = req.localAddress;
+ ex.localPort = req.localPort;
+ }
+
+ return ex;
+}
+
+function _afterConnectMultiple(
+ context,
+ current,
+ status,
+ handle,
+ req,
+ readable,
+ writable,
+) {
+ debug(
+ "connect/multiple: connection attempt to %s:%s completed with status %s",
+ req.address,
+ req.port,
+ status,
+ );
+
+ // Make sure another connection is not spawned
+ clearTimeout(context[kTimeout]);
+
+ // One of the connection has completed and correctly dispatched but after timeout, ignore this one
+ if (status === 0 && current !== context.current - 1) {
+ debug(
+ "connect/multiple: ignoring successful but timedout connection to %s:%s",
+ req.address,
+ req.port,
+ );
+ handle.close();
+ return;
+ }
+
+ const self = context.socket;
+
+ // Some error occurred, add to the list of exceptions
+ if (status !== 0) {
+ const ex = _createConnectionError(req, status);
+ ArrayPrototypePush(context.errors, ex);
+
+ self.emit(
+ "connectionAttemptFailed",
+ req.address,
+ req.port,
+ req.addressType,
+ ex,
+ );
+
+ // Try the next address, unless we were aborted
+ if (context.socket.connecting) {
+ _internalConnectMultiple(context, status === UV_ECANCELED);
+ }
+
+ return;
+ }
+
+ _afterConnect(status, self._handle, req, readable, writable);
+}
+
+function _internalConnectMultipleTimeout(context, req, handle) {
+ debug(
+ "connect/multiple: connection to %s:%s timed out",
+ req.address,
+ req.port,
+ );
+ context.socket.emit(
+ "connectionAttemptTimeout",
+ req.address,
+ req.port,
+ req.addressType,
+ );
+
+ req.oncomplete = undefined;
+ ArrayPrototypePush(context.errors, _createConnectionError(req, UV_ETIMEDOUT));
+ handle.close();
+
+ // Try the next address, unless we were aborted
+ if (context.socket.connecting) {
+ _internalConnectMultiple(context);
+ }
+}
+
function _checkBindError(err: number, port: number, handle: TCP) {
// EADDRINUSE may not be reported until we call `listen()` or `connect()`.
// To complicate matters, a failed `bind()` followed by `listen()` or `connect()`
@@ -495,6 +611,131 @@ function _internalConnect(
}
}
+function _internalConnectMultiple(context, canceled?: boolean) {
+ clearTimeout(context[kTimeout]);
+ const self = context.socket;
+
+ // We were requested to abort. Stop all operations
+ if (self._aborted) {
+ return;
+ }
+
+ // All connections have been tried without success, destroy with error
+ if (canceled || context.current === context.addresses.length) {
+ if (context.errors.length === 0) {
+ self.destroy(new ERR_SOCKET_CONNECTION_TIMEOUT());
+ return;
+ }
+
+ self.destroy(new NodeAggregateError(context.errors));
+ return;
+ }
+
+ assert(self.connecting);
+
+ const current = context.current++;
+
+ if (current > 0) {
+ self[kReinitializeHandle](new TCP(TCPConstants.SOCKET));
+ }
+
+ const { localPort, port, flags } = context;
+ const { address, family: addressType } = context.addresses[current];
+ let localAddress;
+ let err;
+
+ if (localPort) {
+ if (addressType === 4) {
+ localAddress = DEFAULT_IPV4_ADDR;
+ err = self._handle.bind(localAddress, localPort);
+ } else { // addressType === 6
+ localAddress = DEFAULT_IPV6_ADDR;
+ err = self._handle.bind6(localAddress, localPort, flags);
+ }
+
+ debug(
+ "connect/multiple: binding to localAddress: %s and localPort: %d (addressType: %d)",
+ localAddress,
+ localPort,
+ addressType,
+ );
+
+ err = _checkBindError(err, localPort, self._handle);
+ if (err) {
+ ArrayPrototypePush(
+ context.errors,
+ exceptionWithHostPort(err, "bind", localAddress, localPort),
+ );
+ _internalConnectMultiple(context);
+ return;
+ }
+ }
+
+ debug(
+ "connect/multiple: attempting to connect to %s:%d (addressType: %d)",
+ address,
+ port,
+ addressType,
+ );
+ self.emit("connectionAttempt", address, port, addressType);
+
+ const req = new TCPConnectWrap();
+ req.oncomplete = FunctionPrototypeBind(
+ _afterConnectMultiple,
+ undefined,
+ context,
+ current,
+ );
+ req.address = address;
+ req.port = port;
+ req.localAddress = localAddress;
+ req.localPort = localPort;
+ req.addressType = addressType;
+
+ ArrayPrototypePush(
+ self.autoSelectFamilyAttemptedAddresses,
+ `${address}:${port}`,
+ );
+
+ if (addressType === 4) {
+ err = self._handle.connect(req, address, port);
+ } else {
+ err = self._handle.connect6(req, address, port);
+ }
+
+ if (err) {
+ const sockname = self._getsockname();
+ let details;
+
+ if (sockname) {
+ details = sockname.address + ":" + sockname.port;
+ }
+
+ const ex = exceptionWithHostPort(err, "connect", address, port, details);
+ ArrayPrototypePush(context.errors, ex);
+
+ self.emit("connectionAttemptFailed", address, port, addressType, ex);
+ _internalConnectMultiple(context);
+ return;
+ }
+
+ if (current < context.addresses.length - 1) {
+ debug(
+ "connect/multiple: setting the attempt timeout to %d ms",
+ context.timeout,
+ );
+
+ // If the attempt has not returned an error, start the connection timer
+ context[kTimeout] = setTimeout(
+ _internalConnectMultipleTimeout,
+ context.timeout,
+ context,
+ req,
+ self._handle,
+ );
+ }
+}
+
// Provide a better error message when we call end() as a result
// of the other side sending a FIN. The standard "write after end"
// is overly vague, and makes it seem like the user's code is to blame.
@@ -597,7 +838,7 @@ function _lookupAndConnect(
) {
const { localAddress, localPort } = options;
const host = options.host || "localhost";
- let { port } = options;
+ let { port, autoSelectFamilyAttemptTimeout, autoSelectFamily } = options;
if (localAddress && !isIP(localAddress)) {
throw new ERR_INVALID_IP_ADDRESS(localAddress);
@@ -621,6 +862,22 @@ function _lookupAndConnect(
port |= 0;
+ if (autoSelectFamily != null) {
+ validateBoolean(autoSelectFamily, "options.autoSelectFamily");
+ } else {
+ autoSelectFamily = autoSelectFamilyDefault;
+ }
+
+ if (autoSelectFamilyAttemptTimeout !== undefined) {
+ validateInt32(autoSelectFamilyAttemptTimeout);
+
+ if (autoSelectFamilyAttemptTimeout < 10) {
+ autoSelectFamilyAttemptTimeout = 10;
+ }
+ } else {
+ autoSelectFamilyAttemptTimeout = autoSelectFamilyAttemptTimeoutDefault;
+ }
+
// If host is an IP, skip performing a lookup
const addressType = isIP(host);
if (addressType) {
@@ -649,6 +906,7 @@ function _lookupAndConnect(
const dnsOpts = {
family: options.family,
hints: options.hints || 0,
+ all: false,
};
if (
@@ -665,6 +923,31 @@ function _lookupAndConnect(
self._host = host;
const lookup = options.lookup || dnsLookup;
+ if (
+ dnsOpts.family !== 4 && dnsOpts.family !== 6 && !localAddress &&
+ autoSelectFamily
+ ) {
+ debug("connect: autodetecting");
+
+ dnsOpts.all = true;
+ defaultTriggerAsyncIdScope(self[asyncIdSymbol], function () {
+ _lookupAndConnectMultiple(
+ self,
+ asyncIdSymbol,
+ lookup,
+ host,
+ options,
+ dnsOpts,
+ port,
+ localAddress,
+ localPort,
+ autoSelectFamilyAttemptTimeout,
+ );
+ });
+
+ return;
+ }
+
defaultTriggerAsyncIdScope(self[asyncIdSymbol], function () {
lookup(
host,
@@ -719,6 +1002,143 @@ function _lookupAndConnect(
});
}
+function _lookupAndConnectMultiple(
+ self: Socket,
+ asyncIdSymbol: number,
+ // deno-lint-ignore no-explicit-any
+ lookup: any,
+ host: string,
+ options: TcpSocketConnectOptions,
+ dnsopts,
+ port: number,
+ localAddress: string,
+ localPort: number,
+ timeout: number | undefined,
+) {
+ defaultTriggerAsyncIdScope(self[asyncIdSymbol], function emitLookup() {
+ lookup(host, dnsopts, function emitLookup(err, addresses) {
+ // It's possible we were destroyed while looking this up.
+ // XXX it would be great if we could cancel the promise returned by
+ // the look up.
+ if (!self.connecting) {
+ return;
+ } else if (err) {
+ self.emit("lookup", err, undefined, undefined, host);
+
+ // net.createConnection() creates a net.Socket object and immediately
+ // calls net.Socket.connect() on it (that's us). There are no event
+ // listeners registered yet so defer the error event to the next tick.
+ nextTick(_connectErrorNT, self, err);
+ return;
+ }
+
+ // Filter addresses by only keeping the one which are either IPv4 or IPV6.
+ // The first valid address determines which group has preference on the
+ // alternate family sorting which happens later.
+ const validAddresses = [[], []];
+ const validIps = [[], []];
+ let destinations;
+ for (let i = 0, l = addresses.length; i < l; i++) {
+ const address = addresses[i];
+ const { address: ip, family: addressType } = address;
+ self.emit("lookup", err, ip, addressType, host);
+ // It's possible we were destroyed while looking this up.
+ if (!self.connecting) {
+ return;
+ }
+ if (isIP(ip) && (addressType === 4 || addressType === 6)) {
+ destinations ||= addressType === 6 ? { 6: 0, 4: 1 } : { 4: 0, 6: 1 };
+
+ const destination = destinations[addressType];
+
+ // Only try an address once
+ if (!ArrayPrototypeIncludes(validIps[destination], ip)) {
+ ArrayPrototypePush(validAddresses[destination], address);
+ ArrayPrototypePush(validIps[destination], ip);
+ }
+ }
+ }
+
+ // When no AAAA or A records are available, fail on the first one
+ if (!validAddresses[0].length && !validAddresses[1].length) {
+ const { address: firstIp, family: firstAddressType } = addresses[0];
+
+ if (!isIP(firstIp)) {
+ err = new ERR_INVALID_IP_ADDRESS(firstIp);
+ nextTick(_connectErrorNT, self, err);
+ } else if (firstAddressType !== 4 && firstAddressType !== 6) {
+ err = new ERR_INVALID_ADDRESS_FAMILY(
+ firstAddressType,
+ options.host,
+ options.port,
+ );
+ nextTick(_connectErrorNT, self, err);
+ }
+
+ return;
+ }
+
+ // Sort addresses alternating families
+ const toAttempt = [];
+ for (
+ let i = 0,
+ l = MathMax(validAddresses[0].length, validAddresses[1].length);
+ i < l;
+ i++
+ ) {
+ if (i in validAddresses[0]) {
+ ArrayPrototypePush(toAttempt, validAddresses[0][i]);
+ }
+ if (i in validAddresses[1]) {
+ ArrayPrototypePush(toAttempt, validAddresses[1][i]);
+ }
+ }
+
+ if (toAttempt.length === 1) {
+ debug(
+ "connect/multiple: only one address found, switching back to single connection",
+ );
+ const { address: ip, family: addressType } = toAttempt[0];
+
+ self._unrefTimer();
+ defaultTriggerAsyncIdScope(
+ self[asyncIdSymbol],
+ _internalConnect,
+ self,
+ ip,
+ port,
+ addressType,
+ localAddress,
+ localPort,
+ );
+
+ return;
+ }
+
+ self.autoSelectFamilyAttemptedAddresses = [];
+ debug("connect/multiple: will try the following addresses", toAttempt);
+
+ const context = {
+ socket: self,
+ addresses: toAttempt,
+ current: 0,
+ port,
+ localPort,
+ timeout,
+ [kTimeout]: null,
+ errors: [],
+ };
+
+ self._unrefTimer();
+ defaultTriggerAsyncIdScope(
+ self[asyncIdSymbol],
+ _internalConnectMultiple,
+ context,
+ );
+ });
+ });
+}
+
function _afterShutdown(this: ShutdownWrap<TCP>) {
// deno-lint-ignore no-explicit-any
const self: any = this.handle[ownerSymbol];
@@ -777,6 +1197,7 @@ export class Socket extends Duplex {
_host: string | null = null;
// deno-lint-ignore no-explicit-any
_parent: any = null;
+ autoSelectFamilyAttemptedAddresses: AddressInfo[] | undefined = undefined;
constructor(options: SocketOptions | number) {
if (typeof options === "number") {
@@ -1546,6 +1967,16 @@ export class Socket extends Duplex {
set _handle(v: Handle | null) {
this[kHandle] = v;
}
+
+ // deno-lint-ignore no-explicit-any
+ [kReinitializeHandle](handle: any) {
+ this._handle?.close();
+
+ this._handle = handle;
+ this._handle[ownerSymbol] = this;
+
+ _initSocketHandle(this);
+ }
}
export const Stream = Socket;
@@ -1593,6 +2024,33 @@ export function connect(...args: unknown[]) {
export const createConnection = connect;
+/** https://docs.deno.com/api/node/net/#namespace_getdefaultautoselectfamily */
+export function getDefaultAutoSelectFamily() {
+ return autoSelectFamilyDefault;
+}
+
+/** https://docs.deno.com/api/node/net/#namespace_setdefaultautoselectfamily */
+export function setDefaultAutoSelectFamily(value: boolean) {
+ validateBoolean(value, "value");
+ autoSelectFamilyDefault = value;
+}
+
+/** https://docs.deno.com/api/node/net/#namespace_getdefaultautoselectfamilyattempttimeout */
+export function getDefaultAutoSelectFamilyAttemptTimeout() {
+ return autoSelectFamilyAttemptTimeoutDefault;
+}
+
+/** https://docs.deno.com/api/node/net/#namespace_setdefaultautoselectfamilyattempttimeout */
+export function setDefaultAutoSelectFamilyAttemptTimeout(value: number) {
+ validateInt32(value, "value", 1);
+
+ if (value < 10) {
+ value = 10;
+ }
+
+ autoSelectFamilyAttemptTimeoutDefault = value;
+}
+
export interface ListenOptions extends Abortable {
fd?: number;
port?: number | undefined;
@@ -2478,15 +2936,19 @@ export { BlockList, isIP, isIPv4, isIPv6, SocketAddress };
export default {
_createServerHandle,
_normalizeArgs,
- isIP,
- isIPv4,
- isIPv6,
BlockList,
- SocketAddress,
connect,
createConnection,
createServer,
+ getDefaultAutoSelectFamily,
+ getDefaultAutoSelectFamilyAttemptTimeout,
+ isIP,
+ isIPv4,
+ isIPv6,
Server,
+ setDefaultAutoSelectFamily,
+ setDefaultAutoSelectFamilyAttemptTimeout,
Socket,
+ SocketAddress,
Stream,
};
diff --git a/tests/node_compat/config.jsonc b/tests/node_compat/config.jsonc
index 16951d9ed..664adaedf 100644
--- a/tests/node_compat/config.jsonc
+++ b/tests/node_compat/config.jsonc
@@ -77,6 +77,7 @@
"test-fs-rmdir-recursive.js",
"test-fs-write-file.js",
"test-http-url.parse-https.request.js",
+ "test-net-autoselectfamily.js",
"test-net-better-error-messages-path.js",
"test-net-connect-buffer.js",
"test-net-connect-buffer2.js",
@@ -404,6 +405,7 @@
"test-http-url.parse-only-support-http-https-protocol.js",
"test-icu-transcode.js",
"test-net-access-byteswritten.js",
+ "test-net-autoselectfamily.js",
"test-net-better-error-messages-listen-path.js",
"test-net-better-error-messages-path.js",
"test-net-better-error-messages-port-hostname.js",
diff --git a/tests/node_compat/runner/TODO.md b/tests/node_compat/runner/TODO.md
index 231a4f62c..27c2ef3e7 100644
--- a/tests/node_compat/runner/TODO.md
+++ b/tests/node_compat/runner/TODO.md
@@ -1767,7 +1767,6 @@ NOTE: This file should not be manually edited. Please edit `tests/node_compat/co
- [parallel/test-net-autoselectfamily-commandline-option.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-autoselectfamily-commandline-option.js)
- [parallel/test-net-autoselectfamily-default.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-autoselectfamily-default.js)
- [parallel/test-net-autoselectfamily-ipv4first.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-autoselectfamily-ipv4first.js)
-- [parallel/test-net-autoselectfamily.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-autoselectfamily.js)
- [parallel/test-net-better-error-messages-listen.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-better-error-messages-listen.js)
- [parallel/test-net-binary.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-binary.js)
- [parallel/test-net-bind-twice.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-bind-twice.js)
diff --git a/tests/node_compat/test/common/index.js b/tests/node_compat/test/common/index.js
index d2165aecd..d358ffce5 100644
--- a/tests/node_compat/test/common/index.js
+++ b/tests/node_compat/test/common/index.js
@@ -473,6 +473,7 @@ const pwdCommand = isWindows ?
module.exports = {
allowGlobals,
+ defaultAutoSelectFamilyAttemptTimeout: 2500,
expectsError,
expectWarning,
getArrayBufferViews,
diff --git a/tests/node_compat/test/parallel/test-net-autoselectfamily.js b/tests/node_compat/test/parallel/test-net-autoselectfamily.js
new file mode 100644
index 000000000..3b520e6c8
--- /dev/null
+++ b/tests/node_compat/test/parallel/test-net-autoselectfamily.js
@@ -0,0 +1,312 @@
+// deno-fmt-ignore-file
+// deno-lint-ignore-file
+
+// Copyright Joyent and Node contributors. All rights reserved. MIT license.
+// Taken from Node 18.12.1
+// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually.
+
+'use strict';
+
+const common = require('../common');
+const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
+
+const assert = require('assert');
+const dgram = require('dgram');
+const { Resolver } = require('dns');
+const { createConnection, createServer } = require('net');
+
+// Test that happy eyeballs algorithm is properly implemented.
+
+// Purposely not using setDefaultAutoSelectFamilyAttemptTimeout here to test the
+// parameter is correctly used in options.
+//
+// Some of the machines in the CI need more time to establish connection
+const autoSelectFamilyAttemptTimeout = common.defaultAutoSelectFamilyAttemptTimeout;
+
+function _lookup(resolver, hostname, options, cb) {
+ resolver.resolve(hostname, 'ANY', (err, replies) => {
+ assert.notStrictEqual(options.family, 4);
+
+ if (err) {
+ return cb(err);
+ }
+
+ const hosts = replies
+ .map((r) => ({ address: r.address, family: r.type === 'AAAA' ? 6 : 4 }))
+ .sort((a, b) => b.family - a.family);
+
+ if (options.all === true) {
+ return cb(null, hosts);
+ }
+
+ return cb(null, hosts[0].address, hosts[0].family);
+ });
+}
+
+function createDnsServer(ipv6Addrs, ipv4Addrs, cb) {
+ if (!Array.isArray(ipv6Addrs)) {
+ ipv6Addrs = [ipv6Addrs];
+ }
+
+ if (!Array.isArray(ipv4Addrs)) {
+ ipv4Addrs = [ipv4Addrs];
+ }
+
+ // Create a DNS server which replies with a AAAA and a A record for the same host
+ const socket = dgram.createSocket('udp4');
+
+ // TODO(kt3k): We use common.mustCallAtLeast instead of common.mustCall
+ // because Deno sends multiple requests to the DNS server.
+ // This can be addressed if Deno.resolveDns supports ANY record type.
+ // See https://github.com/denoland/deno/issues/14492
+ socket.on('message', common.mustCallAtLeast((msg, { address, port }) => {
+ const parsed = parseDNSPacket(msg);
+ const domain = parsed.questions[0].domain;
+ assert.strictEqual(domain, 'example.org');
+
+ socket.send(writeDNSPacket({
+ id: parsed.id,
+ questions: parsed.questions,
+ answers: [
+ ...ipv6Addrs.map((address) => ({ type: 'AAAA', address, ttl: 123, domain: 'example.org' })),
+ ...ipv4Addrs.map((address) => ({ type: 'A', address, ttl: 123, domain: 'example.org' })),
+ ]
+ }), port, address);
+ }));
+
+ socket.bind(0, () => {
+ const resolver = new Resolver();
+ resolver.setServers([`127.0.0.1:${socket.address().port}`]);
+
+ cb({ dnsServer: socket, lookup: _lookup.bind(null, resolver) });
+ });
+}
+
+// Test that IPV4 is reached if IPV6 is not reachable
+{
+ createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
+ const ipv4Server = createServer((socket) => {
+ socket.on('data', common.mustCall(() => {
+ socket.write('response-ipv4');
+ socket.end();
+ }));
+ });
+
+ ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
+ const port = ipv4Server.address().port;
+
+ const connection = createConnection({
+ host: 'example.org',
+ port: port,
+ lookup,
+ autoSelectFamily: true,
+ autoSelectFamilyAttemptTimeout,
+ });
+
+ let response = '';
+ connection.setEncoding('utf-8');
+
+ connection.on('ready', common.mustCall(() => {
+ assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`, `127.0.0.1:${port}`]);
+ }));
+
+ connection.on('data', (chunk) => {
+ response += chunk;
+ });
+
+ connection.on('end', common.mustCall(() => {
+ assert.strictEqual(response, 'response-ipv4');
+ ipv4Server.close();
+ dnsServer.close();
+ }));
+
+ connection.write('request');
+ }));
+ }));
+}
+
+// Test that only the last successful connection is established.
+{
+ createDnsServer(
+ ['2606:4700::6810:85e5', '2606:4700::6810:84e5', "::1"],
+ // TODO(kt3k): Comment out ipv4 addresses to make the test pass faster.
+ // Enable this when Deno.connect() call becomes cancellable.
+ // See https://github.com/denoland/deno/issues/26819
+ // ['104.20.22.46', '104.20.23.46', '127.0.0.1'],
+ ['127.0.0.1'],
+ common.mustCall(function({ dnsServer, lookup }) {
+ const ipv4Server = createServer((socket) => {
+ socket.on('data', common.mustCall(() => {
+ socket.write('response-ipv4');
+ socket.end();
+ }));
+ });
+
+ ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
+ const port = ipv4Server.address().port;
+
+ const connection = createConnection({
+ host: 'example.org',
+ port: port,
+ lookup,
+ autoSelectFamily: true,
+ autoSelectFamilyAttemptTimeout,
+ });
+
+ let response = '';
+ connection.setEncoding('utf-8');
+
+ connection.on('ready', common.mustCall(() => {
+ assert.deepStrictEqual(
+ connection.autoSelectFamilyAttemptedAddresses,
+ [
+ `2606:4700::6810:85e5:${port}`,
+ `104.20.22.46:${port}`,
+ `2606:4700::6810:84e5:${port}`,
+ `104.20.23.46:${port}`,
+ `::1:${port}`,
+ `127.0.0.1:${port}`,
+ ]
+ );
+ }));
+
+ connection.on('data', (chunk) => {
+ response += chunk;
+ });
+
+ connection.on('end', common.mustCall(() => {
+ assert.strictEqual(response, 'response-ipv4');
+ ipv4Server.close();
+ dnsServer.close();
+ }));
+
+ connection.write('request');
+ }));
+ })
+ );
+}
+
+// Test that IPV4 is NOT reached if IPV6 is reachable
+if (common.hasIPv6) {
+ createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
+ const ipv4Server = createServer((socket) => {
+ socket.on('data', common.mustNotCall(() => {
+ socket.write('response-ipv4');
+ socket.end();
+ }));
+ });
+
+ const ipv6Server = createServer((socket) => {
+ socket.on('data', common.mustCall(() => {
+ socket.write('response-ipv6');
+ socket.end();
+ }));
+ });
+
+ ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
+ const port = ipv4Server.address().port;
+
+ ipv6Server.listen(port, '::1', common.mustCall(() => {
+ const connection = createConnection({
+ host: 'example.org',
+ port,
+ lookup,
+ autoSelectFamily: true,
+ autoSelectFamilyAttemptTimeout,
+ });
+
+ let response = '';
+ connection.setEncoding('utf-8');
+
+ connection.on('ready', common.mustCall(() => {
+ assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`]);
+ }));
+
+ connection.on('data', (chunk) => {
+ response += chunk;
+ });
+
+ connection.on('end', common.mustCall(() => {
+ assert.strictEqual(response, 'response-ipv6');
+ ipv4Server.close();
+ ipv6Server.close();
+ dnsServer.close();
+ }));
+
+ connection.write('request');
+ }));
+ }));
+ }));
+}
+
+// Test that when all errors are returned when no connections succeeded
+{
+ createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
+ const connection = createConnection({
+ host: 'example.org',
+ port: 10,
+ lookup,
+ autoSelectFamily: true,
+ autoSelectFamilyAttemptTimeout,
+ });
+
+ connection.on('ready', common.mustNotCall());
+ connection.on('error', common.mustCall((error) => {
+ assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, ['::1:10', '127.0.0.1:10']);
+ assert.strictEqual(error.constructor.name, 'AggregateError');
+ assert.strictEqual(error.errors.length, 2);
+
+ const errors = error.errors.map((e) => e.message);
+ assert.ok(errors.includes('connect ECONNREFUSED 127.0.0.1:10'));
+
+ if (common.hasIPv6) {
+ assert.ok(errors.includes('connect ECONNREFUSED ::1:10'));
+ }
+
+ dnsServer.close();
+ }));
+ }));
+}
+
+// Test that the option can be disabled
+{
+ createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
+ const ipv4Server = createServer((socket) => {
+ socket.on('data', common.mustCall(() => {
+ socket.write('response-ipv4');
+ socket.end();
+ }));
+ });
+
+ ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
+ const port = ipv4Server.address().port;
+
+ const connection = createConnection({
+ host: 'example.org',
+ port,
+ lookup,
+ autoSelectFamily: false,
+ });
+
+ connection.on('ready', common.mustNotCall());
+ connection.on('error', common.mustCall((error) => {
+ assert.strictEqual(connection.autoSelectFamilyAttemptedAddresses, undefined);
+
+ if (common.hasIPv6) {
+ assert.strictEqual(error.code, 'ECONNREFUSED');
+ assert.strictEqual(error.message, `connect ECONNREFUSED ::1:${port}`);
+ } else if (error.code === 'EAFNOSUPPORT') {
+ assert.strictEqual(error.message, `connect EAFNOSUPPORT ::1:${port} - Local (undefined:undefined)`);
+ } else if (error.code === 'EUNATCH') {
+ assert.strictEqual(error.message, `connect EUNATCH ::1:${port} - Local (:::0)`);
+ } else {
+ assert.strictEqual(error.code, 'EADDRNOTAVAIL');
+ assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`);
+ }
+
+ ipv4Server.close();
+ dnsServer.close();
+ }));
+ }));
+ }));
+}
diff --git a/tests/unit_node/http2_test.ts b/tests/unit_node/http2_test.ts
index 7473a487a..c540c90f7 100644
--- a/tests/unit_node/http2_test.ts
+++ b/tests/unit_node/http2_test.ts
@@ -10,6 +10,11 @@ import * as net from "node:net";
import { assert, assertEquals } from "@std/assert";
import { curlRequest } from "../unit/test_util.ts";
+// Increase the timeout for the auto select family to avoid flakiness
+net.setDefaultAutoSelectFamilyAttemptTimeout(
+ net.getDefaultAutoSelectFamilyAttemptTimeout() * 30,
+);
+
for (const url of ["http://localhost:4246", "https://localhost:4247"]) {
Deno.test(`[node/http2 client] ${url}`, {
ignore: Deno.build.os === "windows",