Skip to content

Commit b7c45b3

Browse files
nrakoclaude
andcommitted
fix(node/net): return string family in server.address()
Aligns Deno's `server.address()` with Node.js v18.4.0+ behavior by returning the `family` property as a string ("IPv4" or "IPv6") rather than a number. 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. Refs: - nodejs/node#43054 - nodejs/node@70b516e 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent efa4da8 commit b7c45b3

File tree

8 files changed

+163
-14
lines changed

8 files changed

+163
-14
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 & 1 deletion
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 { isIP } 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,9 +2233,12 @@ export class ServerImpl extends EventEmitter {
22322233

22332234
address() {
22342235
if (this.#addr === null) return null;
2236+
const addr = this.#addr.hostname;
2237+
const ipVersion = isIP(addr);
22352238
return {
22362239
port: this.#addr.port,
2237-
address: this.#addr.hostname,
2240+
address: addr,
2241+
family: ipVersion === 6 ? "IPv6" : "IPv4",
22382242
};
22392243
}
22402244
}

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_binding/tcp_wrap.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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,8 @@ 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+
const ipVersion = isIP(remoteAddr.hostname);
147+
this.#remoteFamily = ipVersion === 6 ? "IPv6" : "IPv4";
147148
}
148149
}
149150

@@ -279,7 +280,8 @@ export class TCP extends ConnectionWrap {
279280

280281
sockname.address = this.#address;
281282
sockname.port = this.#port;
282-
sockname.family = isIP(this.#address);
283+
const ipVersion = isIP(this.#address);
284+
sockname.family = ipVersion === 6 ? "IPv6" : "IPv4";
283285

284286
return 0;
285287
}
@@ -375,7 +377,8 @@ export class TCP extends ConnectionWrap {
375377
#connect(req: TCPConnectWrap, address: string, port: number): number {
376378
this.#remoteAddress = address;
377379
this.#remotePort = port;
378-
this.#remoteFamily = isIP(address);
380+
const ipVersion = isIP(address);
381+
this.#remoteFamily = ipVersion === 6 ? "IPv6" : "IPv4";
379382

380383
op_net_connect_tcp(
381384
{ 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/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)