-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
normalize HMR upgrade path comparison #5678
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1860,16 +1860,50 @@ class Server { | |
| (this.options.webSocketServer).options | ||
| ).path; | ||
|
|
||
| // Normalize a URL path for HMR-path comparison: lowercase, decode | ||
| // percent-encoding (so `/%77%73` matches `/ws`), and strip trailing | ||
| // slashes. Returns null when decoding fails on malformed percent escapes. | ||
| /** | ||
| * @param {string} pathToNormalize URL path to normalize for HMR comparison. | ||
| * @returns {string | null} Normalized path, or null on decode failure. | ||
| */ | ||
| const normalizeHmrPath = (pathToNormalize) => { | ||
| try { | ||
| return decodeURIComponent(pathToNormalize) | ||
| .toLowerCase() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the routes should be case-sensitive, just like Express handles them by default.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment also comes from how ws handles its internal server, and partly from how I remember Express handling routes. |
||
| .replace(/\/+$/, ""); | ||
| } catch { | ||
| return null; | ||
| } | ||
| }; | ||
|
|
||
| const normalizedHmrPath = hmrPath ? normalizeHmrPath(hmrPath) : null; | ||
|
|
||
| for (const webSocketProxy of webSocketProxies) { | ||
| const proxyUpgrade = | ||
| /** @type {RequestHandler & { upgrade: NonNullable<RequestHandler["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) { | ||
| if ( | ||
| normalizedHmrPath && | ||
| typeof req.url === "string" && | ||
| req.url.startsWith("/") | ||
| ) { | ||
| // Collapse leading multiple-slashes so `//ws` is not parsed as a | ||
| // scheme-relative URL (which would yield `pathname === "/"` and | ||
| // bypass the HMR-path filter). | ||
| const cleanUrl = req.url.replace(/^\/+/, "/"); | ||
| let pathname; | ||
| try { | ||
| ({ pathname } = new URL(cleanUrl, "http://0.0.0.0")); | ||
| } catch { | ||
| // Malformed URL: fail closed (do not forward to proxy). | ||
| return; | ||
| } | ||
| const normalized = normalizeHmrPath(pathname); | ||
| if (normalized !== null && normalized === normalizedHmrPath) { | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -859,6 +859,90 @@ describe("proxy option", () => { | |
| }); | ||
| }); | ||
|
|
||
| describe("HMR upgrade path matching tolerates URL variants of the configured path", () => { | ||
| let server; | ||
| let backend; | ||
| let backendUpgradeCount; | ||
|
|
||
| beforeAll(async () => { | ||
| backendUpgradeCount = 0; | ||
|
|
||
| backend = http.createServer(); | ||
| new WebSocketServer({ server: backend }).on("connection", () => { | ||
| backendUpgradeCount += 1; | ||
| }); | ||
|
|
||
| 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 () => { | ||
| backend.closeAllConnections(); | ||
| await server.stop(); | ||
| await new Promise((resolve) => { | ||
| backend.close(resolve); | ||
| }); | ||
| }); | ||
|
|
||
| // The HMR server's default path is /ws. These variants should all be | ||
| // recognized as the HMR socket and not forwarded through the user proxy. | ||
| const variants = [ | ||
| ["trailing slash", "/ws/"], | ||
| ["uppercase", "/WS"], | ||
| ["mixed case", "/wS"], | ||
| ["percent-encoded path", "/%77%73"], | ||
|
Comment on lines
+913
to
+915
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All of these should go to the proxy if we implement case-sensitive routes. |
||
| ["leading double slash", "//ws"], | ||
| ]; | ||
|
|
||
| it.each(variants)( | ||
| "treats %s (%s) as the HMR upgrade path", | ||
| async (_label, path) => { | ||
| const before = backendUpgradeCount; | ||
|
|
||
| const ws = new WebSocket(`ws://localhost:${port3}${path}`); | ||
| ws.on("error", () => {}); | ||
|
|
||
| await new Promise((resolve) => { | ||
| setTimeout(resolve, 400); | ||
| }); | ||
|
|
||
| try { | ||
| ws.close(); | ||
| } catch { | ||
| // ignore close errors on already-failed sockets | ||
| } | ||
|
|
||
| await new Promise((resolve) => { | ||
| setTimeout(resolve, 200); | ||
| }); | ||
|
|
||
| expect(backendUpgradeCount).toBe(before); | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| describe("should supports http methods", () => { | ||
| let server; | ||
| let req; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By normalizing this ourselves, how much would it affect performance? It might be worth running some benchmarks, just as a side note, since I don't remember whether decodeURIComponent is expensive performance-wise.