Skip to content

Commit e44bdae

Browse files
fix(overlay): keep a runtime error raised during the initial load (#5745)
* 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 * test(overlay): cover invalid/ok overlay dismissal in the client
1 parent 1dd13c6 commit e44bdae

7 files changed

Lines changed: 114 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack-dev-server": patch
3+
---
4+
5+
Keep the overlay for a runtime error thrown while the page is still loading.

‎client-src/index.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,8 @@ const onSocketMessage = {
451451
log.info("App updated. Recompiling...");
452452

453453
// Fixes #1042. overlay doesn't clear if errors are fixed but warnings remain.
454+
// A rebuild replaces the code a runtime error came from too, so unlike
455+
// `ok`/`still-ok` this clears that overlay as well.
454456
if (options.overlay) {
455457
overlay.send({ type: "DISMISS" });
456458
}
@@ -521,7 +523,7 @@ const onSocketMessage = {
521523
log.info("Nothing changed.");
522524

523525
if (options.overlay) {
524-
overlay.send({ type: "DISMISS" });
526+
overlay.send({ type: "BUILD_OK" });
525527
}
526528

527529
sendMessage("StillOk");
@@ -530,7 +532,7 @@ const onSocketMessage = {
530532
sendMessage("Ok");
531533

532534
if (options.overlay) {
533-
overlay.send({ type: "DISMISS" });
535+
overlay.send({ type: "BUILD_OK" });
534536
}
535537

536538
reloadApp(options, status);

‎client-src/overlay.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,15 @@ const createOverlayMachine = (options) => {
145145
target: "hidden",
146146
actions: ["dismissMessages", "hideOverlay"],
147147
},
148+
// WHY: A compilation reporting success says nothing about a runtime
149+
// error, so it clears build errors only — no state but this one
150+
// handles it, and an unhandled event is a no-op. Dismissing on the
151+
// initial handshake's `ok` is what closed an overlay raised while
152+
// the page was still loading (#5024).
153+
BUILD_OK: {
154+
target: "hidden",
155+
actions: ["dismissMessages", "hideOverlay"],
156+
},
148157
BUILD_ERROR: {
149158
target: "displayBuildError",
150159
actions: ["appendMessages", "showOverlay"],

‎test/client/index.test.js‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,15 @@ describe("index", () => {
9898

9999
t.assert.snapshot(log.log.info.mock.calls[1][0]);
100100
t.assert.snapshot(sendMessage.mock.calls[0][0]);
101-
expect(overlay.send).not.toHaveBeenCalledWith({ type: "DISMISS" });
101+
expect(overlay.send).not.toHaveBeenCalledWith({ type: "BUILD_OK" });
102102

103103
// change flags
104104
onSocketMessage.overlay(true);
105105
onSocketMessage["still-ok"]();
106106

107-
expect(overlay.send).toHaveBeenCalledWith({ type: "DISMISS" });
107+
// An unchanged compilation clears a build error only, never a runtime one.
108+
expect(overlay.send).toHaveBeenCalledWith({ type: "BUILD_OK" });
109+
expect(overlay.send).not.toHaveBeenCalledWith({ type: "DISMISS" });
108110
});
109111

110112
it("should run onSocketMessage.progress and onSocketMessage['progress-update']", (t) => {
@@ -158,6 +160,21 @@ describe("index", () => {
158160
expect(res).toBeUndefined();
159161
});
160162

163+
it("should clear only build errors when a compilation succeeds", () => {
164+
onSocketMessage.overlay(true);
165+
166+
// A rebuild replaces the code a runtime error came from, so it clears both.
167+
onSocketMessage.invalid();
168+
169+
expect(overlay.send).toHaveBeenCalledWith({ type: "DISMISS" });
170+
171+
// A successful compilation says nothing about a runtime error — see #5024.
172+
onSocketMessage.ok();
173+
174+
expect(overlay.send).toHaveBeenCalledWith({ type: "BUILD_OK" });
175+
expect(overlay.send.mock.calls).toHaveLength(2);
176+
});
177+
161178
it("should run onSocketMessage['static-changed']", (t) => {
162179
onSocketMessage["static-changed"]();
163180

‎test/client/overlay-lifecycle.test.js‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ const buildError = (messages) => ({
1111
messages,
1212
});
1313

14+
const runtimeError = (messages) => ({
15+
type: "RUNTIME_ERROR",
16+
level: "error",
17+
messages,
18+
});
19+
1420
const loadOverlay = () => {
1521
const iframe = document.querySelector("#webpack-dev-server-client-overlay");
1622

@@ -68,6 +74,29 @@ describe("overlay lifecycle", () => {
6874
expect(document.body.contains(iframe)).toBe(false);
6975
});
7076

77+
it("lets a successful compilation clear a build error but not a runtime one", () => {
78+
const overlay = createOverlay({});
79+
80+
overlay.send(buildError(["build error"]));
81+
const buildIframe = loadOverlay();
82+
83+
overlay.send({ type: "BUILD_OK" });
84+
expect(document.body.contains(buildIframe)).toBe(false);
85+
86+
overlay.send(runtimeError(["runtime error"]));
87+
const runtimeIframe = loadOverlay();
88+
89+
// The compilation succeeded, so it knows nothing about this error — see #5024.
90+
overlay.send({ type: "BUILD_OK" });
91+
expect(document.body.contains(runtimeIframe)).toBe(true);
92+
expect(runtimeIframe.contentDocument.body.textContent).toContain(
93+
"runtime error",
94+
);
95+
96+
overlay.send({ type: "DISMISS" });
97+
expect(document.body.contains(runtimeIframe)).toBe(false);
98+
});
99+
71100
it("reuses its Trusted Types policy and encodes editor paths", () => {
72101
const createPolicy = fn(() => ({ createHTML: (value) => value }));
73102
globalThis.trustedTypes = { createPolicy };

‎test/e2e/overlay.test.js‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1709,6 +1709,51 @@ describe("overlay", () => {
17091709
}
17101710
});
17111711

1712+
it("should keep the overlay for a runtime error thrown during the initial load", async () => {
1713+
const compiler = webpack({
1714+
...config,
1715+
entry: "./throw-on-initial-load.js",
1716+
});
1717+
1718+
const server = new Server(
1719+
{
1720+
port,
1721+
},
1722+
compiler,
1723+
);
1724+
1725+
await server.start();
1726+
1727+
const { page, browser } = await runBrowser();
1728+
1729+
try {
1730+
await page.goto(`http://localhost:${port}/`, {
1731+
waitUntil: "networkidle0",
1732+
});
1733+
1734+
// The entry throws while the page is still loading, so the overlay is up
1735+
// before the socket handshake reports the (successful) compilation. That
1736+
// `ok` used to dismiss it milliseconds later — see #5024.
1737+
await waitForExpect(async () => {
1738+
const overlayHandle = await page.$(
1739+
"#webpack-dev-server-client-overlay",
1740+
);
1741+
1742+
expect(overlayHandle).not.toBeNull();
1743+
1744+
const overlayFrame = await overlayHandle.contentFrame();
1745+
const overlayText = await overlayFrame.evaluate(
1746+
() => document.body.textContent,
1747+
);
1748+
1749+
expect(overlayText).toContain("Injected error");
1750+
});
1751+
} finally {
1752+
await browser.close();
1753+
await server.stop();
1754+
}
1755+
});
1756+
17121757
it("should not show filtered runtime error", async () => {
17131758
const compiler = webpack(config);
17141759

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Throws while the entry is still evaluating, i.e. before the client's socket
2+
// handshake completes. Compilation succeeds, so the handshake reports `ok`.
3+
throw new Error("Injected error");

0 commit comments

Comments
 (0)