From d77b34bcabe5b41e25384c9d343d55ce5ae5825a Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 13 Jul 2026 22:11:41 -0500 Subject: [PATCH 1/2] fix(client): avoid double slash when joining dynamicPublicPath When output.publicPath ends with a slash (the common case) and the SSE path starts with one, the naive concatenation produced URLs like `https://host//__webpack_hmr`, which never match the server-side pathname check. Ref webpack/webpack-hot-middleware#366 --- client-src/index.js | 8 +++++++- test/client.test.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/client-src/index.js b/client-src/index.js index 0cbf56bf1..c58a5f848 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -113,7 +113,13 @@ function setOverrides(overrides) { } if (overrides.dynamicPublicPath) { - options.path = __webpack_public_path__ + options.path; + // Avoid a double slash when the public path has a trailing slash and the + // SSE path a leading one (e.g. "https://host/" + "/__webpack_hmr"). + const publicPath = __webpack_public_path__; + options.path = + publicPath.endsWith("/") && options.path.startsWith("/") + ? publicPath + options.path.slice(1) + : publicPath + options.path; } setLogLevel(options.logging); diff --git a/test/client.test.js b/test/client.test.js index 763d62436..79e2008da 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -702,6 +702,36 @@ describe("client", () => { }); }); + describe("with dynamicPublicPath", () => { + let EventSourceStub; + + beforeEach(() => { + EventSourceStub = makeEventSourceStub(); + globalThis.EventSource = EventSourceStub; + jest.spyOn(console, "info").mockImplementation(() => {}); + jest.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(() => { + delete globalThis.__webpack_public_path__; + jest.restoreAllMocks(); + }); + + it("prepends the public path to the SSE path", () => { + globalThis.__webpack_public_path__ = "/assets"; + loadClient("?dynamicPublicPath=true"); + expect(EventSourceStub.lastInstance().url).toBe("/assets/__webpack_hmr"); + }); + + it("does not produce a double slash when the public path has a trailing slash", () => { + globalThis.__webpack_public_path__ = "https://localhost:3000/assets/"; + loadClient("?dynamicPublicPath=true"); + expect(EventSourceStub.lastInstance().url).toBe( + "https://localhost:3000/assets/__webpack_hmr", + ); + }); + }); + describe("connection lifecycle", () => { let EventSourceStub; let client; From 9af10a74e873b9b11328e9955b4fccaeb889bd27 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 20 Jul 2026 17:33:36 -0500 Subject: [PATCH 2/2] fix(client): append path to the public path like a filename in dynamicPublicPath The default `path` starts with "/", so the naive concatenation with a public path ending in "/" produced URLs like `https://host//__webpack_hmr`, which never match the server-side pathname check. Strip the leading slash from `path` and concatenate without any other normalization: the public path itself is left untouched (intentional double slashes, e.g. nginx rewrites, are preserved) and is expected to end with "/". Ref webpack/webpack-hot-middleware#366 --- README.md | 2 +- client-src/index.js | 10 +++------- test/client.test.js | 12 ++++++++++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index daacd04e6..2eff576fe 100644 --- a/README.md +++ b/README.md @@ -384,7 +384,7 @@ entry: [ | `logging` | `string` | `"info"` | Logger level — one of `"none"`, `"error"`, `"warn"`, `"info"`, `"log"`, `"verbose"`. Uses webpack's runtime logger. | | `name` | `string` | `""` | Restrict updates to a specific compilation name (useful with multi-compiler). | | `autoConnect` | `boolean` | `true` | Connect on load; set to `false` and call `setOptionsAndConnect()` manually. | -| `dynamicPublicPath` | `boolean` | `false` | Prefix `path` with `__webpack_public_path__` at runtime. | +| `dynamicPublicPath` | `boolean` | `false` | Prefix `path` with `__webpack_public_path__` at runtime. The leading slash of `path` is stripped and no other normalization is applied, so the public path should end with `/`. | ### Programmatic API diff --git a/client-src/index.js b/client-src/index.js index c58a5f848..75bbc608a 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -113,13 +113,9 @@ function setOverrides(overrides) { } if (overrides.dynamicPublicPath) { - // Avoid a double slash when the public path has a trailing slash and the - // SSE path a leading one (e.g. "https://host/" + "/__webpack_hmr"). - const publicPath = __webpack_public_path__; - options.path = - publicPath.endsWith("/") && options.path.startsWith("/") - ? publicPath + options.path.slice(1) - : publicPath + options.path; + // `path` is appended like a filename (no leading slash); the public path + // itself is not normalized. + options.path = __webpack_public_path__ + options.path.replace(/^\//, ""); } setLogLevel(options.logging); diff --git a/test/client.test.js b/test/client.test.js index 79e2008da..34e05d24f 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -717,12 +717,20 @@ describe("client", () => { jest.restoreAllMocks(); }); - it("prepends the public path to the SSE path", () => { - globalThis.__webpack_public_path__ = "/assets"; + it("appends the SSE path to the public path like webpack appends filenames", () => { + globalThis.__webpack_public_path__ = "/assets/"; loadClient("?dynamicPublicPath=true"); expect(EventSourceStub.lastInstance().url).toBe("/assets/__webpack_hmr"); }); + it("preserves intentional double slashes inside the public path", () => { + globalThis.__webpack_public_path__ = "https://host//rewritten/"; + loadClient("?dynamicPublicPath=true"); + expect(EventSourceStub.lastInstance().url).toBe( + "https://host//rewritten/__webpack_hmr", + ); + }); + it("does not produce a double slash when the public path has a trailing slash", () => { globalThis.__webpack_public_path__ = "https://localhost:3000/assets/"; loadClient("?dynamicPublicPath=true");