balderdashy / sails

Realtime MVC Framework for Node.js
https://sailsjs.com
MIT License
22.84k stars 1.95k forks source link

Sails.io.js WebSocket only works intermittently #6994

Open anthozep opened 4 years ago

anthozep commented 4 years ago

Node version: 12.14.1 Sails version (sails): 1.2.4 ORM hook version (sails-hook-orm): 2.1.1 Sockets hook version (sails-hook-sockets): 2.0.0 Organics hook version (sails-hook-organics): N/A Grunt hook version (sails-hook-grunt): 3.1.0 Uploads hook version (sails-hook-uploads): N/A DB adapter & version (e.g. sails-mysql@5.55.5): sails-mysql@1.0.1 Skipper adapter & version (e.g. skipper-s3@5.55.5): N/A


Hi,

I have a sails.js application that uses websockets (sails.io) to communicate with a React application.

Most of the time, the socket fails to connect. The error in the React console on the frontend is:

WebSocket connection to 'ws:<URL>/socket.io/?__sails_io_sdk_version=1.2.1&__sails_io_sdk_platform=node&__sails_io_sdk_language=javascript&EIO=3&transport=websocket' failed: WebSocket is closed before the connection is established.
 ====================================
sails.io.js:420  The socket was unable to connect.
sails.io.js:420  The server may be offline, or the
sails.io.js:420  socket may have failed authorization
sails.io.js:420  based on its origin or other factors.
sails.io.js:420  You may want to check the values of
sails.io.js:420  `sails.config.sockets.onlyAllowOrigins`
sails.io.js:420  or (more rarely) `sails.config.sockets.beforeConnect`
sails.io.js:420  in your app.
sails.io.js:420  More info: https://sailsjs.com/config/sockets
sails.io.js:420  For help: https://sailsjs.com/support
sails.io.js:420  
sails.io.js:420  Technical details:
sails.io.js:420  Error: websocket error
    at WS.push../node_modules/engine.io-client/lib/transport.js.Transport.onError (transport.js:68)
    at WebSocket.ws.onerror (websocket.js:159)
    at WS.push../node_modules/engine.io-client/lib/transports/websocket.js.WS.doClose (websocket.js:244)
    at WS.push../node_modules/engine.io-client/lib/transport.js.Transport.close (transport.js:98)
    at Socket.push../node_modules/engine.io-client/lib/socket.js.Socket.onClose (socket.js:713)
    at close (socket.js:661)
    at Socket.push../node_modules/engine.io-client/lib/socket.js.Socket.close (socket.js:656)
    at manager.js:263

I have implemented a simple beforeConnect in the backend that looks like this:

  beforeConnect: function(handshake, proceed) {

    // Send back `true` to allow the socket to connect.
    // (Or send back `false` to reject the attempt.)
    console.log(handshake);
    console.log(proceed);
    return proceed(undefined, true);

  },

This should simply show the handshake and continue.

When the connection is successful, which is random and maybe happens 10% of the time, this works as expected and shows the handshake object. I then see that the frontend has connected in the console.

When it does not work, the beforeConnect function doesn't even seem to be called, so I'm not sure that the connection is even reaching the sails app. Since I set io.socket._raw.io._reconnection to true, it tries to reconnect, but again, the sails app doesn't seem to get the connection.

I should note that the same thing works both locally and in production on a cloud foundry service.

For more info, the front end code:

let socketIOClient = require('socket.io-client');
let sailsIOClient = require('sails.io.js');
let io = sailsIOClient(socketIOClient);

// This function is called when the app starts
const initSocket = () => {
    io.sails.url = currentURL; // Set the base URL for sockets
    io.socket._raw.io._reconnection = true;
}

const update = (data) => {
    let roomId = uuidv4();
    let request = {data: data, roomId: roomId};

    store.dispatch(Actions.updateStatus.reset());

    try {
        io.socket.post('/update', request, () => {
            toastr.info('Starting update');
        });

        io.socket.on('success', (success) => {
            store.dispatch(Actions.updateStatus.addSuccess(success.data));
        });

        io.socket.on('failure', (failure) => {
            store.dispatch(Actions.updateStatus.addFailure(failure.data));
        });

        io.socket.on('done', () => {
            io.socket.post('/finishUpdate', {roomId: roomId}, () => {
                toastr.info('Done updating');
                store.dispatch(Actions.updateStatus.signalComplete());
            })
        });
    } catch (err) {
        toastr.error('Socket Failure');
        return;
    }

}

Backend:

pdeUpdate: async function (req, res) {

    let data = req.body.data;
    let updatedBy = 'unknown';

    if (!req.isSocket) {
        sails.log.error('request was not a socket: ' + JSON.stringify(req));
        return res.badRequest();
    }

    var roomId = req.body.roomId;

    sails.sockets.join(req, roomId, function(err) {
        if (err) {
          return res.serverError(err);
        }

        return res.json({
          message: `Subscribed to ${roomId}`
        });
    });
    outerData:
    for (let item in data) {
        try {
            let result = await sails.sendNativeQuery('CALL update($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14);', [... (args here)]);
            if (typeof result.rows !== 'undefined') {
                if (typeof result.rows[0] !== 'undefined') {
                    for (let row in result.rows[0]) {
                        if (typeof result.rows[0][row]['@full_error'] !== 'undefined') {
                            sails.sockets.broadcast(roomId, 'failure', {result: 'failure', data: data[item].name});
                            continue outerData;
                        }
                    }
                }
            }
            if (typeof result !== 'undefined'){
                sails.sockets.broadcast(roomId, 'success', {result: 'success', data: data[item].name});
            }
        } catch (err) {
            sails.log.error('error on row ' + JSON.stringify(data[item]) + err);
            sails.sockets.broadcast(roomId, 'failure', {result: 'failure', data: data[item].name});
            continue;
        }
    }
    sails.sockets.broadcast(roomId, 'done', {message: 'done'});
},

finishUpdate: async function (req, res) {
    let roomId = req.body.roomId;

    sails.sockets.leave(req, roomId, function(err) {
        if (err) {
          return res.serverError(err);
        }

        return res.json({
          message: `done with ${roomId}`
        });
    });
}
sailsbot commented 4 years ago

@anthozep Thanks for posting! We'll take a look as soon as possible.

In the mean time, there are a few ways you can help speed things along:

Please remember: never post in a public forum if you believe you've found a genuine security vulnerability. Instead, disclose it responsibly.

For help with questions about Sails, click here.

johnabrams7 commented 4 years ago

Hi @anthozep Thanks for all the detailed info on this issue. Can you make us an empty Sails repo that reproduces this issue without any of the business logic?

anthozep commented 4 years ago

Sure, this will take a bit of time, but I'm on it