Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions lib/Server.js
Original file line number Diff line number Diff line change
Expand Up @@ -1850,13 +1850,31 @@ class Server {
/** @type {RequestHandler[]} */
(this.webSocketProxies);

const hmrPath =
this.options.webSocketServer &&
/** @type {WebSocketServerConfiguration} */
(this.options.webSocketServer).options &&
/** @type {NonNullable<WebSocketServerConfiguration["options"]>} */
(
/** @type {WebSocketServerConfiguration} */
(this.options.webSocketServer).options
).path;

for (const webSocketProxy of webSocketProxies) {
/** @type {S} */
(this.server).on(
"upgrade",
const proxyUpgrade =
/** @type {RequestHandler & { upgrade: NonNullable<RequestHandler["upgrade"]> }} */
(webSocketProxy).upgrade,
);
(webSocketProxy).upgrade;

/** @type {S} */
(this.server).on("upgrade", (req, socket, head) => {
if (hmrPath && req.url) {
const { pathname } = new URL(req.url, "http://0.0.0.0");
if (pathname === hmrPath) {
return;
}
}
proxyUpgrade(req, socket, head);
});
}
}

Expand Down
4 changes: 2 additions & 2 deletions test/e2e/__snapshots__/api.test.js.snap.webpack5
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ exports[`API Server.checkHostHeader should allow URLs with scheme for checking o
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
"[HMR] Waiting for update signal from WDS...",
"Hey.",
"WebSocket connection to 'ws://test.host:8158/ws' failed: Error in connection establishment: net::ERR_NAME_NOT_RESOLVED",
"WebSocket connection to 'ws://test.host:8159/ws' failed: Error in connection establishment: net::ERR_NAME_NOT_RESOLVED",
"[webpack-dev-server] JSHandle@object",
"[webpack-dev-server] Disconnected!",
"[webpack-dev-server] Trying to reconnect...",
Expand All @@ -40,7 +40,7 @@ exports[`API Server.checkHostHeader should allow URLs with scheme for checking o

exports[`API Server.checkHostHeader should allow URLs with scheme for checking origin when the "option.client.webSocketURL" is object: response status 1`] = `200`;

exports[`API Server.checkHostHeader should allow URLs with scheme for checking origin when the "option.client.webSocketURL" is object: web socket URL 1`] = `"ws://test.host:8158/ws"`;
exports[`API Server.checkHostHeader should allow URLs with scheme for checking origin when the "option.client.webSocketURL" is object: web socket URL 1`] = `"ws://test.host:8159/ws"`;

exports[`API Server.getFreePort should retry finding the port for up to defaultPortRetry times (number): console messages 1`] = `
[
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/__snapshots__/client-reconnect.test.js.snap.webpack5
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ exports[`client.reconnect option specified as number should try to reconnect 2 t
"Hey.",
"[webpack-dev-server] Disconnected!",
"[webpack-dev-server] Trying to reconnect...",
"WebSocket connection to 'ws://localhost:8163/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED",
"WebSocket connection to 'ws://localhost:8164/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED",
"[webpack-dev-server] JSHandle@object",
"[webpack-dev-server] Trying to reconnect...",
"WebSocket connection to 'ws://localhost:8163/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED",
"WebSocket connection to 'ws://localhost:8164/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED",
"[webpack-dev-server] JSHandle@object",
]
`;
Expand Down
8 changes: 4 additions & 4 deletions test/e2e/__snapshots__/port.test.js.snap.webpack5
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,25 @@ exports[`port should work using "0" port : console messages 1`] = `

exports[`port should work using "0" port : page errors 1`] = `[]`;

exports[`port should work using "8161" port : console messages 1`] = `
exports[`port should work using "8162" port : console messages 1`] = `
[
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
"[HMR] Waiting for update signal from WDS...",
"Hey.",
]
`;

exports[`port should work using "8161" port : console messages 2`] = `
exports[`port should work using "8162" port : console messages 2`] = `
[
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
"[HMR] Waiting for update signal from WDS...",
"Hey.",
]
`;

exports[`port should work using "8161" port : page errors 1`] = `[]`;
exports[`port should work using "8162" port : page errors 1`] = `[]`;

exports[`port should work using "8161" port : page errors 2`] = `[]`;
exports[`port should work using "8162" port : page errors 2`] = `[]`;

exports[`port should work using "auto" port : console messages 1`] = `
[
Expand Down
2 changes: 1 addition & 1 deletion test/ports-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const listOfTests = {
"on-listening-option": 1,
"open-option": 1,
"port-option": 1,
"proxy-option": 4,
"proxy-option": 5,
server: 1,
"setup-exit-signals-option": 1,
"static-directory-option": 1,
Expand Down
202 changes: 201 additions & 1 deletion test/server/proxy-option.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";

const http = require("node:http");
const path = require("node:path");
const util = require("node:util");
const express = require("express");
Expand All @@ -8,7 +9,8 @@ const webpack = require("webpack");
const WebSocket = require("ws");
const Server = require("../../lib/Server");
const config = require("../fixtures/proxy-config/webpack.config");
const [port1, port2, port3, port4] = require("../ports-map")["proxy-option"];
const [port1, port2, port3, port4, port5] =
require("../ports-map")["proxy-option"];

const WebSocketServer = WebSocket.Server;
const staticDirectory = path.resolve(__dirname, "../fixtures/proxy-config");
Expand Down Expand Up @@ -659,6 +661,204 @@ describe("proxy option", () => {
}
});

describe("should not silently proxy dev-server HMR websocket to a permissive backend", () => {
let server;
let backend;
let backendWss;
let backendUpgradeCount;

const BACKEND_MESSAGE_TYPE = "backend-message";

beforeAll(async () => {
backendUpgradeCount = 0;

backend = http.createServer();
backendWss = new WebSocketServer({ server: backend });
backendWss.on("connection", (connection) => {
backendUpgradeCount += 1;
connection.send(JSON.stringify({ type: BACKEND_MESSAGE_TYPE }));
});

await new Promise((resolve) => {
backend.listen(port5, resolve);
});

const compiler = webpack(config);

server = new Server(
{
hot: true,
allowedHosts: "all",
webSocketServer: "ws",
proxy: [
{
context: "/",
target: `http://localhost:${port5}`,
ws: true,
},
],
port: port3,
},
compiler,
);

await server.start();
});

afterAll(async () => {
for (const client of backendWss.clients) {
client.terminate();
}
backendWss.close();
// Force-drop any lingering proxy-opened sockets so backend.close() does
// not hang when the fix is missing and the proxy is mid-upgrade.
backend.closeAllConnections();
await server.stop();
await new Promise((resolve) => {
backend.close(resolve);
});
});

it("delivers the HMR control messages and never reaches the proxy target", async () => {
const messages = [];

const ws = new WebSocket(`ws://localhost:${port3}/ws`);

await new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(
new Error(
`Timed out waiting for HMR message. Got: ${JSON.stringify(messages)}`,
),
);
}, 3000);

ws.on("message", (raw) => {
const parsed = JSON.parse(raw.toString());
messages.push(parsed);
if (parsed.type === "hot") {
clearTimeout(timer);
resolve();
}
});

ws.on("error", (err) => {
clearTimeout(timer);
reject(err);
});
});

ws.close();

// Let the proxy finish its async forwarding so the assertion below sees
// the upgrade attempt deterministically.
await new Promise((resolve) => {
setTimeout(resolve, 300);
});

expect(messages.some((m) => m.type === "hot")).toBe(true);
expect(messages.some((m) => m.type === BACKEND_MESSAGE_TYPE)).toBe(false);
expect(backendUpgradeCount).toBe(0);
});
});

describe("should not log proxy errors for the dev-server HMR upgrade", () => {
let server;
let backend;
let stderrSpy;

beforeAll(async () => {
stderrSpy = jest
.spyOn(process.stderr, "write")
.mockImplementation(() => true);

backend = http.createServer();
backend.on("upgrade", (req, socket) => {
socket.destroy();
});
await new Promise((resolve) => {
backend.listen(port5, resolve);
});

const compiler = webpack(config);

server = new Server(
{
hot: true,
allowedHosts: "all",
webSocketServer: "ws",
proxy: [
{
context: "/",
target: `http://localhost:${port5}`,
ws: true,
},
],
port: port3,
},
compiler,
);

await server.start();
});

afterAll(async () => {
stderrSpy.mockRestore();
backend.closeAllConnections();
await server.stop();
await new Promise((resolve) => {
backend.close(resolve);
});
});

it("does not surface any [HPM] error when the HMR client connects", async () => {
const messages = [];

const ws = new WebSocket(`ws://localhost:${port3}/ws`);

await new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(
new Error(
`Timed out waiting for HMR message. Got: ${JSON.stringify(messages)}`,
),
);
}, 3000);

ws.on("message", (raw) => {
const parsed = JSON.parse(raw.toString());
messages.push(parsed);
if (parsed.type === "hot") {
clearTimeout(timer);
resolve();
}
});

ws.on("error", (err) => {
clearTimeout(timer);
reject(err);
});
});

ws.close();

await new Promise((resolve) => {
setTimeout(resolve, 200);
});

const hpmLines = stderrSpy.mock.calls
.map((c) => c[0])
.join("")
.split("\n")
.filter((line) => line.includes("[HPM]"))
.map((line) => line.replaceAll(/localhost:\d+/g, "localhost:<port>"))
.join("\n");

expect(hpmLines).toBe("");
expect(messages.some((m) => m.type === "hot")).toBe(true);
});
});

describe("should supports http methods", () => {
let server;
let req;
Expand Down
Loading