6

Is there a way to have a separate websocket server work alongside socket.io on a different path?

let http = require('http');
let express = require('express');
let socketio = require('socket.io');
let websocket = require('ws');

let httpServer = http.createServer();

let expressApp = express();
httpServer.on('request', expressApp);

let socketioServer = socketio(httpServer, { path: '/aaaaa/socket.io/' });
socketioServer.of('/').on('connect', () => {});

let websocketServer = new websocket.Server({ server: httpServer, path: '/aaaaa/graphql' });

httpServer.listen(2233, () => console.log('started'));

The behavior I'm seeing is that, when a separate websocket server is created, socket.io still functions properly, but will not uprade connections to a websocket and fails with the error (from chrome):

WebSocket connection to 'ws://localhost:2233/aaaaa/socket.io/?EIO=3&transport=websocket&sid=fx4pOT0cegz65JMCAAAB' failed: Invalid frame header

To be clear, if the websocket server line is omitted, socket.io works properly.

My specific use-case is that a websocket server is created by the apollo-server-express package when subscriptions are enabled. Is there a way to have socket.io configured in a more friendly way? Or, I believe I can supply a websocket server for apollo to use instead of creating one... how would I create that?

Package versions for reproduction:

node       8.11.1
express    4.16.4
socket.io  2.1.1
ws         6.1.0
Trevor Wilson
  • 193
  • 2
  • 7
  • That's not what this is supposed to accomplish. I want to have a socket.io client connect to the socket.io server, and I want a websocket client to connect to the websocket server. I'm not looking for cross-communication. – Trevor Wilson Nov 06 '18 at 22:27
  • Ah, I misunderstood then. So the issue is that `ws` won't delegate socket connections on the path `/aaaaa/socket.io` to `socket.io`? – Patrick Roberts Nov 06 '18 at 22:29
  • 2
    It looks like to get `ws` to play nice, you'll have to [manually implement the `upgrade` event](https://github.com/websockets/ws#multiple-servers-sharing-a-single-https-server) for your server to delegate to the websocket server or socket.io based on the path – Patrick Roberts Nov 06 '18 at 22:34
  • actually, the websocket connections to /aaaaa/graphql seem perfectly fine, its just the socket.io side that is unable to upgrade – Trevor Wilson Nov 06 '18 at 22:34
  • I'll try out that link... – Trevor Wilson Nov 06 '18 at 22:35
  • That's what I'm saying. `ws` is hogging the upgrade request and not properly delegating it to socket.io when the path doesn't match. It's `ws`'s fault, not `socket.io`'s. – Patrick Roberts Nov 06 '18 at 22:35
  • 1
    @PatrickRoberts hey, that worked! It wasn't as cut-and-dry as that link was, but handling the upgrade event manually was the trick. Thank you very much! – Trevor Wilson Nov 06 '18 at 23:35

2 Answers2

4

In case this helps anyone else, here's my derived solution:

let [socketioUpgradeListener, apolloUpgradeListener] = httpServer.listeners('upgrade').slice(0);
httpServer.removeAllListeners('upgrade');
httpServer.on('upgrade', (req, socket, head) => {
  const pathname = url.parse(req.url).pathname;
  if (pathname == '/aaaaa/socket.io/')
    socketioUpgradeListener(req, socket, head);
  else if (pathname == '/aaaaa/graphql')
    apolloUpgradeListener(req, socket, head);
  else
    socket.destroy();
});

Was a bit annoying because both libraries had already fully initialized their websocket servers, with plenty of event listeners, before I could mess with them. However, I could pick out the 'upgrade' listeners and delegate them manually. Of course this isn't perfect since it is sensitive to initialization order and new listeners, but it is adequate for my use-case.

If there's any glaring flaws with this solution or any other nuances with websocket server delegation, please let me know.

Trevor Wilson
  • 193
  • 2
  • 7
  • I do not know how you came to this solution, but I've had same issue with PeerJS and SocketIO together, and I later moved to Primus to find the same issue. Your solution worked like charm for exact same issue with my primus and peerjs application. Would surely love to know how you found this solution. – Sameer Dec 19 '18 at 08:43
  • @Sameer the link given by Patrick Roberts in the comments above put me on the right path – Trevor Wilson Dec 27 '18 at 17:41
2

Had the same problem in NestJs while using graphql- and socket.io-modules in parallel. As an alternative to Trevor's solution you can bind socket.io to another port and use a reverse-proxy like nginx to resolve the paths.

app.gateway.ts

@WebSocketGateway(3001)
export class AppGateway implements OnGatewayConnection {   

  handleConnection(
    client: any,
    payload: any
  ) {
    client.emit('Hi from port 3001');
  }
}

nginx.conf

server {
        listen 80;
        listen [::]:80;
        server_name localhost;

        location /graphql {
                proxy_pass http://127.0.0.1:3000;
        }

        location /socket.io {
                proxy_pass http://127.0.0.1:3001/socket.io/;

                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection 'upgrade';
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header Host $host;

                proxy_http_version 1.1;
        }
}

Of course you can skip the last part and connect to your socket directly via ws://localhost:3001/socket.io on the client-side.