From 25562a3a497e1bc77531834792bd4f0d2f262415 Mon Sep 17 00:00:00 2001 From: alexander-akait <4567934+alexander-akait@users.noreply.github.com> Date: Sat, 19 Sep 2026 14:38:52 +0000 Subject: [PATCH 1/2] fix(overlay): keep a runtime error raised during the initial load An error thrown while the entry is still evaluating raises the overlay before the client's socket handshake completes. The compilation itself succeeded, so the handshake's `ok` arrived milliseconds later and sent `DISMISS`, closing an overlay it knew nothing about. `ok` and `still-ok` now send `BUILD_OK`, which only the build-error state handles. `invalid` keeps sending `DISMISS`: a rebuild replaces the code a runtime error came from, so that overlay is stale too. Closes #5024 --- .../fix-overlay-dismissed-on-initial-load.md | 5 +++ client-src/index.js | 6 ++- client-src/overlay.js | 9 ++++ test/client/index.test.js | 6 ++- test/client/overlay-lifecycle.test.js | 29 ++++++++++++ test/e2e/overlay.test.js | 45 +++++++++++++++++++ .../overlay-config/throw-on-initial-load.js | 3 ++ 7 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 .changeset/fix-overlay-dismissed-on-initial-load.md create mode 100644 test/fixtures/overlay-config/throw-on-initial-load.js diff --git a/.changeset/fix-overlay-dismissed-on-initial-load.md b/.changeset/fix-overlay-dismissed-on-initial-load.md new file mode 100644 index 0000000000..721f809b5b --- /dev/null +++ b/.changeset/fix-overlay-dismissed-on-initial-load.md @@ -0,0 +1,5 @@ +--- +"webpack-dev-server": patch +--- + +Keep the overlay for a runtime error thrown while the page is still loading. diff --git a/client-src/index.js b/client-src/index.js index c8c7adb9f1..59aafb8036 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -451,6 +451,8 @@ const onSocketMessage = { log.info("App updated. Recompiling..."); // Fixes #1042. overlay doesn't clear if errors are fixed but warnings remain. + // A rebuild replaces the code a runtime error came from too, so unlike + // `ok`/`still-ok` this clears that overlay as well. if (options.overlay) { overlay.send({ type: "DISMISS" }); } @@ -521,7 +523,7 @@ const onSocketMessage = { log.info("Nothing changed."); if (options.overlay) { - overlay.send({ type: "DISMISS" }); + overlay.send({ type: "BUILD_OK" }); } sendMessage("StillOk"); @@ -530,7 +532,7 @@ const onSocketMessage = { sendMessage("Ok"); if (options.overlay) { - overlay.send({ type: "DISMISS" }); + overlay.send({ type: "BUILD_OK" }); } reloadApp(options, status); diff --git a/client-src/overlay.js b/client-src/overlay.js index 9ec8ce64ae..4dd3739814 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -145,6 +145,15 @@ const createOverlayMachine = (options) => { target: "hidden", actions: ["dismissMessages", "hideOverlay"], }, + // WHY: A compilation reporting success says nothing about a runtime + // error, so it clears build errors only — no state but this one + // handles it, and an unhandled event is a no-op. Dismissing on the + // initial handshake's `ok` is what closed an overlay raised while + // the page was still loading (#5024). + BUILD_OK: { + target: "hidden", + actions: ["dismissMessages", "hideOverlay"], + }, BUILD_ERROR: { target: "displayBuildError", actions: ["appendMessages", "showOverlay"], diff --git a/test/client/index.test.js b/test/client/index.test.js index 9c2eea09d7..49d9a16aa0 100644 --- a/test/client/index.test.js +++ b/test/client/index.test.js @@ -98,13 +98,15 @@ describe("index", () => { t.assert.snapshot(log.log.info.mock.calls[1][0]); t.assert.snapshot(sendMessage.mock.calls[0][0]); - expect(overlay.send).not.toHaveBeenCalledWith({ type: "DISMISS" }); + expect(overlay.send).not.toHaveBeenCalledWith({ type: "BUILD_OK" }); // change flags onSocketMessage.overlay(true); onSocketMessage["still-ok"](); - expect(overlay.send).toHaveBeenCalledWith({ type: "DISMISS" }); + // An unchanged compilation clears a build error only, never a runtime one. + expect(overlay.send).toHaveBeenCalledWith({ type: "BUILD_OK" }); + expect(overlay.send).not.toHaveBeenCalledWith({ type: "DISMISS" }); }); it("should run onSocketMessage.progress and onSocketMessage['progress-update']", (t) => { diff --git a/test/client/overlay-lifecycle.test.js b/test/client/overlay-lifecycle.test.js index 9b754789df..657b992c1b 100644 --- a/test/client/overlay-lifecycle.test.js +++ b/test/client/overlay-lifecycle.test.js @@ -11,6 +11,12 @@ const buildError = (messages) => ({ messages, }); +const runtimeError = (messages) => ({ + type: "RUNTIME_ERROR", + level: "error", + messages, +}); + const loadOverlay = () => { const iframe = document.querySelector("#webpack-dev-server-client-overlay"); @@ -68,6 +74,29 @@ describe("overlay lifecycle", () => { expect(document.body.contains(iframe)).toBe(false); }); + it("lets a successful compilation clear a build error but not a runtime one", () => { + const overlay = createOverlay({}); + + overlay.send(buildError(["build error"])); + const buildIframe = loadOverlay(); + + overlay.send({ type: "BUILD_OK" }); + expect(document.body.contains(buildIframe)).toBe(false); + + overlay.send(runtimeError(["runtime error"])); + const runtimeIframe = loadOverlay(); + + // The compilation succeeded, so it knows nothing about this error — see #5024. + overlay.send({ type: "BUILD_OK" }); + expect(document.body.contains(runtimeIframe)).toBe(true); + expect(runtimeIframe.contentDocument.body.textContent).toContain( + "runtime error", + ); + + overlay.send({ type: "DISMISS" }); + expect(document.body.contains(runtimeIframe)).toBe(false); + }); + it("reuses its Trusted Types policy and encodes editor paths", () => { const createPolicy = fn(() => ({ createHTML: (value) => value })); globalThis.trustedTypes = { createPolicy }; diff --git a/test/e2e/overlay.test.js b/test/e2e/overlay.test.js index ac719643c8..4c8e2531bc 100644 --- a/test/e2e/overlay.test.js +++ b/test/e2e/overlay.test.js @@ -1709,6 +1709,51 @@ describe("overlay", () => { } }); + it("should keep the overlay for a runtime error thrown during the initial load", async () => { + const compiler = webpack({ + ...config, + entry: "./throw-on-initial-load.js", + }); + + const server = new Server( + { + port, + }, + compiler, + ); + + await server.start(); + + const { page, browser } = await runBrowser(); + + try { + await page.goto(`http://localhost:${port}/`, { + waitUntil: "networkidle0", + }); + + // The entry throws while the page is still loading, so the overlay is up + // before the socket handshake reports the (successful) compilation. That + // `ok` used to dismiss it milliseconds later — see #5024. + await waitForExpect(async () => { + const overlayHandle = await page.$( + "#webpack-dev-server-client-overlay", + ); + + expect(overlayHandle).not.toBeNull(); + + const overlayFrame = await overlayHandle.contentFrame(); + const overlayText = await overlayFrame.evaluate( + () => document.body.textContent, + ); + + expect(overlayText).toContain("Injected error"); + }); + } finally { + await browser.close(); + await server.stop(); + } + }); + it("should not show filtered runtime error", async () => { const compiler = webpack(config); diff --git a/test/fixtures/overlay-config/throw-on-initial-load.js b/test/fixtures/overlay-config/throw-on-initial-load.js new file mode 100644 index 0000000000..85350db264 --- /dev/null +++ b/test/fixtures/overlay-config/throw-on-initial-load.js @@ -0,0 +1,3 @@ +// Throws while the entry is still evaluating, i.e. before the client's socket +// handshake completes. Compilation succeeds, so the handshake reports `ok`. +throw new Error("Injected error"); From f40996e9ca2b7def36f2580b0cae91f850a43e93 Mon Sep 17 00:00:00 2001 From: alexander-akait <4567934+alexander-akait@users.noreply.github.com> Date: Sat, 19 Sep 2026 17:03:49 +0000 Subject: [PATCH 2/2] test(overlay): cover invalid/ok overlay dismissal in the client --- test/client/index.test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/client/index.test.js b/test/client/index.test.js index 49d9a16aa0..98518ea0b8 100644 --- a/test/client/index.test.js +++ b/test/client/index.test.js @@ -160,6 +160,21 @@ describe("index", () => { expect(res).toBeUndefined(); }); + it("should clear only build errors when a compilation succeeds", () => { + onSocketMessage.overlay(true); + + // A rebuild replaces the code a runtime error came from, so it clears both. + onSocketMessage.invalid(); + + expect(overlay.send).toHaveBeenCalledWith({ type: "DISMISS" }); + + // A successful compilation says nothing about a runtime error — see #5024. + onSocketMessage.ok(); + + expect(overlay.send).toHaveBeenCalledWith({ type: "BUILD_OK" }); + expect(overlay.send.mock.calls).toHaveLength(2); + }); + it("should run onSocketMessage['static-changed']", (t) => { onSocketMessage["static-changed"]();