Skip to content

Commit bc9d356

Browse files
nrakoclaudeTango992
authored
fix(node/net): return string family in server.address() (#31465)
## Summary Aligns Deno's Node.js compatibility layer (`node:net`, `node:http`, `node:https`, `node:http2`, `node:dns`) with Node.js v18.4.0+ behavior by returning the `family` property as a string (`"IPv4"` or `"IPv6"`) rather than a number in `server.address()` and socket address methods. Node.js briefly changed `family` from string to number in v18.0.0 (nodejs/node#41431), but reverted in v18.4.0 (nodejs/node#43054) due to ecosystem breakage (nodejs/node#43014). This fix ensures compatibility with npm packages that rely on the string format, which has been the stable API since Node.js v18.4.0. ## Changes - Modified `ext/node/polyfills/http.ts` to add `family` property to `address()` return - Modified `ext/node/polyfills/internal_binding/tcp_wrap.ts` to return string `family` instead of number in `getsockname()`, `getpeername()`, and `#connect()` - Modified `ext/node/polyfills/net.ts` to fix `socket.remoteFamily` getter (no longer needs conversion since `family` is now a string) - Modified `ext/node/polyfills/dns.ts` and `ext/node/polyfills/internal/dns/promises.ts` to accept string family values (`"IPv4"`, `"IPv6"`) in `lookup()`, matching [Node.js behavior](https://nodejs.org/api/dns.html#dnslookuphostname-options-callback) - Added tests in `tests/unit_node/http_test.ts`, `tests/unit_node/http2_test.ts`, `tests/unit_node/https_test.ts`, `tests/unit_node/dns_test.ts`, and `tests/unit_node/net_test.ts` ## Node.js Compatibility Note For non-IP addresses (when `isIP()` returns 0), the `family` property is `undefined`. This matches Node.js C++ behavior in [`AddressToJS`](https://github.com/nodejs/node/blob/main/src/tcp_wrap.cc) where family is only set for `AF_INET` (`"IPv4"`) and `AF_INET6` (`"IPv6"`), and left undefined otherwise. ## Refs - nodejs/node#43054 - nodejs/node@70b516e <!-- Before submitting a PR, please read https://docs.deno.com/runtime/manual/references/contributing 1. Give the PR a descriptive title. Examples of good title: - fix(std/http): Fix race condition in server - docs(console): Update docstrings - feat(doc): Handle nested reexports Examples of bad title: - fix #7123 - update docs - fix bugs 2. Ensure there is a related issue and it is referenced in the PR text. 3. Ensure there are tests that cover the changes. 4. Ensure `cargo test` passes. 5. Ensure `./tools/format.js` passes without changing files. 6. Ensure `./tools/lint.js` passes. 7. Open as a draft PR if your work is still in progress. The CI won't run all steps, but you can add '[ci]' to a commit message to force it to. 8. If you would like to run the benchmarks on the CI, add the 'ci-bench' label. --> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Daniel Rahmanto <daniel.rahmanto@gmail.com>
1 parent 8c46ac3 commit bc9d356

File tree

11 files changed

+194
-18
lines changed

11 files changed

+194
-18
lines changed

ext/node/polyfills/dns.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,19 @@ export function lookup(
234234
}
235235

236236
if (options?.family != null) {
237-
validateOneOf(options.family, "options.family", validFamilies);
238-
family = options.family;
237+
// Accept both numeric (0, 4, 6) and string ('IPv4', 'IPv6') family values
238+
// to match Node.js behavior
239+
switch (options.family) {
240+
case "IPv4":
241+
family = 4;
242+
break;
243+
case "IPv6":
244+
family = 6;
245+
break;
246+
default:
247+
validateOneOf(options.family, "options.family", validFamilies);
248+
family = options.family;
249+
}
239250
}
240251

241252
if (options?.all != null) {

ext/node/polyfills/http.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {
6868
ERR_UNESCAPED_CHARACTERS,
6969
} from "ext:deno_node/internal/errors.ts";
7070
import { getTimerDuration } from "ext:deno_node/internal/timers.mjs";
71+
import { getIPFamily } from "ext:deno_node/internal/net.ts";
7172
import { serve, upgradeHttpRaw } from "ext:deno_http/00_serve.ts";
7273
import { headersEntries } from "ext:deno_fetch/20_headers.js";
7374
import { Response } from "ext:deno_fetch/23_response.js";
@@ -2232,10 +2233,10 @@ export class ServerImpl extends EventEmitter {
22322233

22332234
address() {
22342235
if (this.#addr === null) return null;
2235-
return {
2236-
port: this.#addr.port,
2237-
address: this.#addr.hostname,
2238-
};
2236+
const addr = this.#addr.hostname;
2237+
// Match Node.js: family is undefined for non-IP addresses (isIP returns 0)
2238+
const family = getIPFamily(addr);
2239+
return { port: this.#addr.port, address: addr, family };
22392240
}
22402241
}
22412242

ext/node/polyfills/internal/dns/promises.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,19 @@ export function lookup(
194194
}
195195

196196
if (options?.family != null) {
197-
validateOneOf(options.family, "options.family", validFamilies);
198-
family = options.family;
197+
// Accept both numeric (0, 4, 6) and string ('IPv4', 'IPv6') family values
198+
// to match Node.js behavior
199+
switch (options.family) {
200+
case "IPv4":
201+
family = 4;
202+
break;
203+
case "IPv6":
204+
family = 6;
205+
break;
206+
default:
207+
validateOneOf(options.family, "options.family", validFamilies);
208+
family = options.family;
209+
}
199210
}
200211

201212
if (options?.all != null) {

ext/node/polyfills/internal/net.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ export function isIP(ip: string) {
6666
return 0;
6767
}
6868

69+
export function getIPFamily(ip: string): "IPv4" | "IPv6" | undefined {
70+
if (isIPv4(ip)) {
71+
return "IPv4";
72+
}
73+
if (isIPv6(ip)) {
74+
return "IPv6";
75+
}
76+
return undefined;
77+
}
78+
6979
export function makeSyncWrite(fd: number) {
7080
return function (
7181
// deno-lint-ignore no-explicit-any

ext/node/polyfills/internal_binding/tcp_wrap.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import { ownerSymbol } from "ext:deno_node/internal_binding/symbols.ts";
4343
import { codeMap } from "ext:deno_node/internal_binding/uv.ts";
4444
import { delay } from "ext:deno_node/_util/async.ts";
4545
import { kStreamBaseField } from "ext:deno_node/internal_binding/stream_wrap.ts";
46-
import { isIP } from "ext:deno_node/internal/net.ts";
46+
import { getIPFamily } from "ext:deno_node/internal/net.ts";
4747
import {
4848
ceilPowOf2,
4949
INITIAL_ACCEPT_BACKOFF_DELAY,
@@ -59,7 +59,7 @@ enum socketType {
5959

6060
interface AddressInfo {
6161
address: string;
62-
family?: number;
62+
family?: string;
6363
port: number;
6464
}
6565

@@ -95,7 +95,7 @@ export class TCP extends ConnectionWrap {
9595
#port?: number;
9696

9797
#remoteAddress?: string;
98-
#remoteFamily?: number;
98+
#remoteFamily?: string;
9999
#remotePort?: number;
100100

101101
#backlog?: number;
@@ -143,7 +143,7 @@ export class TCP extends ConnectionWrap {
143143
const remoteAddr = conn.remoteAddr as Deno.NetAddr;
144144
this.#remoteAddress = remoteAddr.hostname;
145145
this.#remotePort = remoteAddr.port;
146-
this.#remoteFamily = isIP(remoteAddr.hostname);
146+
this.#remoteFamily = getIPFamily(remoteAddr.hostname);
147147
}
148148
}
149149

@@ -279,7 +279,7 @@ export class TCP extends ConnectionWrap {
279279

280280
sockname.address = this.#address;
281281
sockname.port = this.#port;
282-
sockname.family = isIP(this.#address);
282+
sockname.family = getIPFamily(this.#address);
283283

284284
return 0;
285285
}
@@ -375,7 +375,7 @@ export class TCP extends ConnectionWrap {
375375
#connect(req: TCPConnectWrap, address: string, port: number): number {
376376
this.#remoteAddress = address;
377377
this.#remotePort = port;
378-
this.#remoteFamily = isIP(address);
378+
this.#remoteFamily = getIPFamily(address);
379379

380380
op_net_connect_tcp(
381381
{ hostname: address ?? "127.0.0.1", port },

ext/node/polyfills/net.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,9 +1531,7 @@ Object.defineProperty(Socket.prototype, "remoteAddress", {
15311531

15321532
Object.defineProperty(Socket.prototype, "remoteFamily", {
15331533
get: function () {
1534-
const { family } = this._getpeername();
1535-
1536-
return family ? `IPv${family}` : family;
1534+
return this._getpeername().family;
15371535
},
15381536
});
15391537

tests/unit_node/dns_test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { assertEquals, fail } from "@std/assert";
33
import dns, { lookupService } from "node:dns";
44
import dnsPromises, {
5+
lookup as lookupPromise,
56
lookupService as lookupServicePromise,
67
} from "node:dns/promises";
78
import { ErrnoException } from "ext:deno_node/_global.d.ts";
@@ -88,3 +89,11 @@ Deno.test("lookupService not found", async () => {
8889
assertEquals((err as ErrnoException).syscall, "getnameinfo");
8990
});
9091
});
92+
93+
Deno.test("[node/dns] lookup accepts string family values", async () => {
94+
const ipv4Result = await lookupPromise("localhost", { family: "IPv4" });
95+
assertEquals(ipv4Result.family, 4);
96+
97+
const ipv6Result = await lookupPromise("localhost", { family: "IPv6" });
98+
assertEquals(ipv6Result.family, 6);
99+
});

tests/unit_node/http2_test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,3 +462,41 @@ Deno.test("internal/http2/util exports", () => {
462462
assert(typeof util.kProxySocket === "symbol");
463463
assert(typeof util.kRequest === "symbol");
464464
});
465+
466+
Deno.test("[node/http2] Server.address() includes family property", async () => {
467+
// Test IPv4
468+
{
469+
const { promise, resolve } = Promise.withResolvers<void>();
470+
const server = http2.createServer((_req, res) => {
471+
res.end("ok");
472+
});
473+
474+
server.listen(0, "127.0.0.1", () => {
475+
const addr = server.address() as net.AddressInfo;
476+
assertEquals(addr.address, "127.0.0.1");
477+
assertEquals(addr.family, "IPv4");
478+
assertEquals(typeof addr.port, "number");
479+
server.close(() => resolve());
480+
});
481+
482+
await promise;
483+
}
484+
485+
// Test IPv6
486+
{
487+
const { promise, resolve } = Promise.withResolvers<void>();
488+
const server = http2.createServer((_req, res) => {
489+
res.end("ok");
490+
});
491+
492+
server.listen(0, "::1", () => {
493+
const addr = server.address() as net.AddressInfo;
494+
assertEquals(addr.address, "::1");
495+
assertEquals(addr.family, "IPv6");
496+
assertEquals(typeof addr.port, "number");
497+
server.close(() => resolve());
498+
});
499+
500+
await promise;
501+
}
502+
});

tests/unit_node/http_test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2224,3 +2224,39 @@ Deno.test("[node/http] client request with empty write in chunked POST completes
22242224
await promise;
22252225
assertEquals(requestBody, "");
22262226
});
2227+
2228+
Deno.test("[node/http] Server.address() includes family property", async () => {
2229+
// Test IPv4
2230+
{
2231+
const { promise, resolve } = Promise.withResolvers<void>();
2232+
const server = http.createServer((_req, res) => res.end("ok"));
2233+
2234+
server.listen(0, "127.0.0.1", () => {
2235+
const addr = server.address();
2236+
assert(addr !== null && typeof addr === "object");
2237+
assertEquals(addr.address, "127.0.0.1");
2238+
assertEquals(addr.family, "IPv4");
2239+
assertEquals(typeof addr.port, "number");
2240+
server.close(() => resolve());
2241+
});
2242+
2243+
await promise;
2244+
}
2245+
2246+
// Test IPv6
2247+
{
2248+
const { promise, resolve } = Promise.withResolvers<void>();
2249+
const server = http.createServer((_req, res) => res.end("ok"));
2250+
2251+
server.listen(0, "::1", () => {
2252+
const addr = server.address();
2253+
assert(addr !== null && typeof addr === "object");
2254+
assertEquals(addr.address, "::1");
2255+
assertEquals(addr.family, "IPv6");
2256+
assertEquals(typeof addr.port, "number");
2257+
server.close(() => resolve());
2258+
});
2259+
2260+
await promise;
2261+
}
2262+
});

tests/unit_node/https_test.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,55 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22

33
import https from "node:https";
4-
import { assert } from "../unit/test_util.ts";
4+
import { assert, assertEquals } from "../unit/test_util.ts";
5+
import type { AddressInfo } from "node:net";
6+
7+
Deno.test("[node/https] Server.address() includes family property", async () => {
8+
const certFile = "tests/testdata/tls/localhost.crt";
9+
const keyFile = "tests/testdata/tls/localhost.key";
10+
11+
// Test IPv4
12+
{
13+
const { promise, resolve } = Promise.withResolvers<void>();
14+
const server = https.createServer({
15+
cert: Deno.readTextFileSync(certFile),
16+
key: Deno.readTextFileSync(keyFile),
17+
}, (_req, res) => {
18+
res.end("ok");
19+
});
20+
21+
server.listen(0, "127.0.0.1", () => {
22+
const addr = server.address() as AddressInfo;
23+
assertEquals(addr.address, "127.0.0.1");
24+
assertEquals(addr.family, "IPv4");
25+
assertEquals(typeof addr.port, "number");
26+
server.close(() => resolve());
27+
});
28+
29+
await promise;
30+
}
31+
32+
// Test IPv6
33+
{
34+
const { promise, resolve } = Promise.withResolvers<void>();
35+
const server = https.createServer({
36+
cert: Deno.readTextFileSync(certFile),
37+
key: Deno.readTextFileSync(keyFile),
38+
}, (_req, res) => {
39+
res.end("ok");
40+
});
41+
42+
server.listen(0, "::1", () => {
43+
const addr = server.address() as AddressInfo;
44+
assertEquals(addr.address, "::1");
45+
assertEquals(addr.family, "IPv6");
46+
assertEquals(typeof addr.port, "number");
47+
server.close(() => resolve());
48+
});
49+
50+
await promise;
51+
}
52+
});
553

654
Deno.test({
755
name:

0 commit comments

Comments
 (0)