parcel-bundler / parcel

The zero configuration build tool for the web. πŸ“¦πŸš€
https://parceljs.org
MIT License
43.41k stars 2.26k forks source link

WebSocket proxy fails if HMR is running on same port #6994

Open bminer opened 3 years ago

bminer commented 3 years ago

πŸ› bug report

When running parcel serve where the --port is the same as the --hmr-port (the default behavior AFAIK), the WebSocket proxy (i.e. .proxyrc) no longer functions properly.

πŸŽ› Configuration

$ cat .proxyrc.json
{
    "/ws": {
        "target": "http://localhost:1235/",
        "ws": true
    }
}

Running this command exhibits the bug:

parcel index.html

πŸ€” Expected Behavior

WebSocket proxy should work. When the browser connects to ws://localhost:1234/ws, it should be proxied to localhost:1235/ws using http-proxy-middleware

😯 Current Behavior

When the browser establishes the WebSocket connection to ws://localhost:1234/ws, parcel indicates that the web browser immediately disconnects:

$ parcel --port 1234 --hmr-port 1234 index.html

Server running at http://localhost:1234
✨ Built in 1.76s
console: [HPM] Upgrading to WebSocket
console: [HPM] Client disconnected

On the WebSocket server running in Go on port 1235, I get an error that seems to indicate that the socket is closed:

failed to get reader: failed to read frame header: EOF

On the browser (Firefox), I get a message like this:

The connection to ws://localhost:1234/ws was interrupted while the page was loading.

which occurs on the line where I create the new WebSocket(...).

πŸ’ Possible Solution

Everything runs fine without parcel. If I parcel build and bypass the parcel server and HMR server, it works.

Running Parcel with a different HMR port also seems to work (note that HMR port is set to 1236):

parcel --port 1234 --hmr-port 1236 index.html
$ parcel --port 1234 --hmr-port 1236 index.html

Server running at http://localhost:1234
✨ Built in 392ms
console: [HPM] Upgrading to WebSocket

... and in this case, it all works splendidly.

This seems to indicate that parcel serve does not properly handle WebSocket proxying with http-proxy-middleware when the HMR server is running on the same port as the normal parcel HTTP server.

πŸ”¦ Context

I'm just trying to run parcel serve [entry_point] and have it work (ideally without a separate HMR port).

πŸ’» Code Sample

I can provide a sample upon request, but it will take time.

🌍 Your Environment

Software Version(s)
Parcel 2.0.0-rc.0
Node v14.17.6
npm/Yarn npm 6.14.15
Operating System Linux Mint 20.2
Browser Firefox 92.0.1
Go 1.16.6
Go WebSocket lib nhooyr.io/websocket v1.8.7
eirikb commented 2 years ago

Thank you. Had the exact same issue, your solution worked perfectly.

mistakenot commented 2 years ago

+1 here also. I'm on parcel ^2.0.1 and socket.io-client ^4.3.2, changing the hmr socket worked for me.

nunof07 commented 2 years ago

Thank you. Your solution worked for me.

dispalt commented 2 years ago

Thank you so much! I was getting something like below (in case others run into this) on the server side (Scala/Java).

io.netty.handler.codec.http.websocketx.CorruptedWebSocketFrameException: bytes are not UTF-8
bminer commented 1 year ago

AFAIK, this is still an issue. Either a documentation change should be made to inform users of this, or the bug should be fixed.

xiemeilong commented 1 year ago

This issue is still exist in the latest version.

mischnic commented 1 year ago

This is the relevant code for the case of --port === --hmr-port.

https://github.com/parcel-bundler/parcel/blob/2facd5df904b3888774a965a6be0e16248006449/packages/reporters/dev-server/src/ServerReporter.js#L46-L55

https://github.com/parcel-bundler/parcel/blob/2facd5df904b3888774a965a6be0e16248006449/packages/reporters/dev-server/src/HMRServer.js#L89

The HTTP server creation including proxyrc is here (the server here becomes devServer in HMRServer above): https://github.com/parcel-bundler/parcel/blob/2facd5df904b3888774a965a6be0e16248006449/packages/reporters/dev-server/src/Server.js#L465-L497

bminer commented 1 year ago

I think the issue is that the HMR server uses ws to create a new WebSocket server with the server option set: https://github.com/parcel-bundler/parcel/blob/2facd5df904b3888774a965a6be0e16248006449/packages/reporters/dev-server/src/HMRServer.js#L89

This basically ensures that this.wss now handles all WebSocket requests for this.options.devServer; thus, only HMR WebSocket requests work and WebSocket proxy requests using http-middleware-proxy are essentially closed preemptively by the HMR WebSocket upgrade handler. Here's the relevant code from the ws library: https://github.com/websockets/ws/blob/06728e444d8f54aa5602b51360f4f98794cb1754/lib/websocket-server.js#L260-L263

The solution here is to tell the HMR WebSocket.Server to run in noServer mode and only handle upgrade events when it's for the HMR. See ws docs here. So line 89 above would change to:

this.wss = new WebSocket.Server({noServer: true});

and then the server's upgrade event would need to be handled manually as shown in this ws usage example:

server.on('upgrade', function upgrade(req, socket, head) {
  let {pathname} = url.parse(req.originalUrl || req.url);
  if (pathname != null && pathname.startsWith(HMR_ENDPOINT)) {
    this.wss.handleUpgrade(req, socket, head, function done(ws) {
      this.wss.emit('connection', ws, req);
    });
  }
  // else, we simply do nothing and hope the request got proxied
});
bminer commented 1 year ago

Anyway, hope this is an easy fix. If you want, I can try my hand at a PR for this.

bminer commented 4 months ago

PR is still open