From 3cca311a10a67e1b9b2d7302b38f78a0812af1f8 Mon Sep 17 00:00:00 2001 From: berce <75228263+bercedev@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:35:07 +0300 Subject: [PATCH 1/2] test: normalize compressed IPv6 addresses in dev server output The IPv6 pattern only matched the full eight-group form, so a unique local address such as [fdb2:2c26:f4e4::1] reached the serve snapshots unchanged, and a full address would have kept its brackets twice. Match the bracketed host in any form, except the [::1] loopback, and replace it with its brackets. --- test/utils/test-utils.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/utils/test-utils.js b/test/utils/test-utils.js index dd7d23e72f5..257bb94166c 100644 --- a/test/utils/test-utils.js +++ b/test/utils/test-utils.js @@ -268,7 +268,9 @@ const normalizeStdout = (stdout) => { }; const IPV4 = /(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})){3}/g; -const IPV6 = /([0-9a-f]){1,4}(:([0-9a-f]){1,4}){7}/gi; +// A bracketed IPv6 host in a URL, in full or compressed ("::") form, e.g. a unique local +// address such as `[fdb2:2c26:f4e4::1]`. The loopback `[::1]` stays as it is. +const IPV6 = /\[(?!::1\])[0-9a-f]*:[0-9a-f:.]*(%[^\]]+)?\]/gi; const normalizeStderr = (stderr) => { if (typeof stderr !== "string") { From a82d38b648607621b70d6fb65708d7fe3a8b77e3 Mon Sep 17 00:00:00 2001 From: berce <75228263+bercedev@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:16:20 +0300 Subject: [PATCH 2/2] test: only replace bracketed text that is an IPv6 address The pattern also matched tokens such as [1:2] or [dead:beef]. Check the bracket contents with net.isIPv6 before replacing them, and cover normalizeStderr's IPv6 handling with unit tests. --- test/api/normalize-stderr.test.js | 33 +++++++++++++++++++++++++++++++ test/utils/test-utils.js | 10 +++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 test/api/normalize-stderr.test.js diff --git a/test/api/normalize-stderr.test.js b/test/api/normalize-stderr.test.js new file mode 100644 index 00000000000..ed0e32d147a --- /dev/null +++ b/test/api/normalize-stderr.test.js @@ -0,0 +1,33 @@ +"use strict"; + +const { normalizeStderr } = require("../utils/test-utils"); + +const NETWORK_IPV6 = " [webpack-dev-server] On Your Network (IPv6): "; + +describe("normalizeStderr", () => { + it("should replace a compressed IPv6 address, such as a unique local address", () => { + expect(normalizeStderr(`${NETWORK_IPV6}http://[fdb2:2c26:f4e4::1]:8080/`)).toBe( + `${NETWORK_IPV6}http://[x:x:x:x:x:x:x:x]:/`, + ); + }); + + it("should replace a full IPv6 address with its brackets", () => { + expect(normalizeStderr(`${NETWORK_IPV6}http://[2001:db8:0:0:0:0:0:1]:8080/`)).toBe( + `${NETWORK_IPV6}http://[x:x:x:x:x:x:x:x]:/`, + ); + }); + + it("should keep the IPv6 loopback", () => { + expect( + normalizeStderr( + " [webpack-dev-server] Loopback: http://localhost:8080/, http://[::1]:8080/", + ), + ).toBe(" [webpack-dev-server] Loopback: http://localhost:/, http://[::1]:/"); + }); + + it("should leave bracketed text that is not an IPv6 address", () => { + expect(normalizeStderr("[1:2] [dead:beef] [webpack-dev-server]")).toBe( + "[1:2] [dead:beef] [webpack-dev-server]", + ); + }); +}); diff --git a/test/utils/test-utils.js b/test/utils/test-utils.js index 257bb94166c..addd613a11b 100644 --- a/test/utils/test-utils.js +++ b/test/utils/test-utils.js @@ -2,6 +2,7 @@ const { exec } = require("node:child_process"); const fs = require("node:fs"); +const { isIPv6 } = require("node:net"); const path = require("node:path"); const { stripVTControlCharacters } = require("node:util"); const concat = require("concat-stream"); @@ -269,8 +270,11 @@ const normalizeStdout = (stdout) => { const IPV4 = /(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})){3}/g; // A bracketed IPv6 host in a URL, in full or compressed ("::") form, e.g. a unique local -// address such as `[fdb2:2c26:f4e4::1]`. The loopback `[::1]` stays as it is. -const IPV6 = /\[(?!::1\])[0-9a-f]*:[0-9a-f:.]*(%[^\]]+)?\]/gi; +// address such as `[fdb2:2c26:f4e4::1]`. The loopback `[::1]` and bracketed text that is not +// an IPv6 address stay as they are. +const IPV6 = /\[([0-9a-f:.]+)(%[^\]]+)?\]/gi; +const replaceIPv6 = (match, address) => + address !== "::1" && isIPv6(address) ? "[x:x:x:x:x:x:x:x]" : match; const normalizeStderr = (stderr) => { if (typeof stderr !== "string") { @@ -285,7 +289,7 @@ const normalizeStderr = (stderr) => { normalizedStderr = normalizeCwd(normalizedStderr); normalizedStderr = normalizedStderr.replaceAll(IPV4, "x.x.x.x"); - normalizedStderr = normalizedStderr.replaceAll(IPV6, "[x:x:x:x:x:x:x:x]"); + normalizedStderr = normalizedStderr.replaceAll(IPV6, replaceIPv6); normalizedStderr = normalizedStderr.replaceAll(/:[0-9]+\//g, ":/"); if (!/On Your Network \(IPv6\)/.test(stderr)) {