Socket.io "Invalid frame header" error with independent websocket server

5.6k views Asked by At

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
2

There are 2 answers

2
Trevor Wilson On

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.

0
Deshi Basara On

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.