Websockets broken on Chrome

Am I missing something? It seems that Chrome is not working correctly with websockets.

Here is what is going on:

On one page, I connect with a websocket. I get my websocket ID back, then I start a “heartbeat” where I send a message _socket.send() every 5 seconds. The socket ID I get matches the one I was given on the connection.

Now I move to another page which opens a websocket. I get back a new socket ID, but when the “heartbeat” starts, I get the old socket ID back. However, at some random interval (could be a few seconds, could be a few minutes) I start getting the new socket ID back.

I spent the past 2 days re writing my user identifier thinking I had a bug or was going mad when I realized this only happens in Chrome and not in Firefox, so I am not sure what is going on.

Lucee: 7.1.0.204
Websocket: 3.0.0.20-SNAPSHOT

Thanks for the detailed writeup — I’ve logged it as LDEV-6457: Jira

Since Firefox returns the correct socket ID immediately and only Chrome returns the stale/previous one for a while, this looks like a connection-reuse/lifecycle issue. A couple of things that would help:

  • A minimal reproducible example (the two pages + the client-side connect/heartbeat code and how you read the socket ID) so we can reproduce it here.
  • Chrome version, and whether the old connection is actually being closed (check chrome://net-export or the Network → WS tab to see if the previous socket lingers).
  • Whether disabling any keep-alive/reconnect logic on the client changes the behavior.

Please follow LDEV-6457 for updates.

I just updated Chrome to the newest version since there was one to do so. So the version I am on atm is: Version 152.0.7977.65 (Official Build) (64-bit)

When the new page load there is no lingering socket. It shows only 1 socket connection going out.

As for the code, it is pretty simple now as I spent some time tearing it apart to figure out what was going wrong. The Websocket.cfc is almost a duplicate of what is posted in the documentation.

Websocket.cfc

component hint="websockets" 
{
    Application.datasourceName = 'mytable';

    Application.setKeyDataPath = "/home/test/domains/dev.test.com/keyData";

    Application.usePBKDF = true;
    Application.password = toBinary( SecretProviderGet("appPassword") );
    Application.salt = toBinary( SecretProviderGet("appSalt") );
    Application.keyRingFilename = toBinary( SecretProviderGet("appKeyRingFilename") );
    Application.keyRingPath = SecretProviderGet("appKeyRingPath") & hash( toString( application.keyRingFilename, 'UTF-8' ), 'MD5', 'UTF-8', 142 ) & '.bin';
    Application.engine = server.coldfusion.productname;
    Application.masterKey = generatePBKDFKey( 'PBKDF2WithHmacSHA1', toString( Application.password, 'UTF-8' ), toString( Application.salt, 'UTF-8' ), 2048, 128 );
    Application.developmentHmacKey = SecretProviderGet("appDevelopmentHmacKey");

    Application.securityService = new cfc.model.services.EncDecService(
            keyRingPath = application.keyRingPath,
            masterKey = application.masterKey
        );

    Application.keyRing = Application.securityService.readKeyRingFromDisk();

    Application.securityService = Application.securityService.init( 
            encryptionKey1          = Application.keyRing[1].key,
            encryptionAlgorithm1    = Application.keyRing[1].alg,
            encryptionEncoding1     = Application.keyRing[1].enc,
            encryptionIV1           = binaryDecode( Application.keyRing[1].iv, 'BASE64' ),
            encryptionKey2          = Application.keyRing[2].key,
            encryptionAlgorithm2    = Application.keyRing[2].alg,
            encryptionEncoding2     = Application.keyRing[2].enc,
            encryptionIV2           = binaryDecode( Application.keyRing[2].iv, 'BASE64' ),
            encryptionKey3          = Application.keyRing[3].key,
            encryptionAlgorithm3    = Application.keyRing[3].alg,
            encryptionEncoding3     = Application.keyRing[3].enc,
            encryptionIV3           = binaryDecode( Application.keyRing[3].iv, 'BASE64' ),
            hmacKey                 = ( ( Application.securityService.getEnvironment() eq 'prod' ) ? generateSecretKey( 'HMACSHA512' ) : application.developmentHmacKey ),
            hmacAlgorithm           = 'HMACSHA512',
            hmacEncoding            = 'UTF-8',
            scN                     = 16384,
            scR                     = 16,
            scP                     = 1
        );

        // clear out temp keys from the application scope
        for( item in [ 'password', 'salt', 'keyRingFilename', 'keyRingPath', 'keyRing',  'masterKey', 'developmentHmacKey', 'usePBKDF' ] ) {
            structDelete( application, item );
        }

    static {
        clientsByUser = {};  // userId -> wsClient
        rolesByUser   = {};  // userId -> array of roles
        dsName = 'mytable'
    }
    
    public static function onFirstOpen( wsclients ) 
    {
        
    }

    function onOpen( wsclient )
    {
        //as of snapshot 3.0.0.20 there is no access to client Session Scope or Server Application Scope.
        //Session in websockets is only the session for that websocket
        //static.wsclients.broadcast("There are now #static.wsclients.size()# connections");
        //onOpen(), we have no way of identifying who just made contact, need to wait for the 1st message with information about user to be sent
        //arguments.wsclient.send( 'incoming' );
        lock name="socketOpen" timeout="30" throwontimeout="no" type="exclusive"
        {

            var userId = getUserIdFromRequest();//really is the socketID

            dt = {}
            dt.type = 0;
            dt.message = '';
            if( userId <= 0)
            {
                dt.message = 'Denied';
                dt.userId = userId;
                arguments.wsClient.send(serializeJSON(dt));
                arguments.wsClient.close();
                return;
            }

            static.clientsByUser[ userId ] = arguments.wsClient;
            //static.rolesByUser[ userId ]   = getRolesForUser( userId );
            arguments.wsClient.send( serializeJSON(wsclient) );
            arguments.wsClient.send( serializeJSON(session) );
            arguments.wsClient.send( serializeJSON(cgi) );
            arguments.wsClient.send( serializeJSON(userId) );
        }
    }

    function onOpenAsync( wsclient )
    {

    }

    function onMessage( wsclient, message )
    {
        if( isJSON(arguments.message))
        {   
            local.msg = deserializeJSON(arguments.message);
            
            //receiving a non init message from client, this should contain no data, just used to keepAlive
            //update users activeCon section
            if( structKeyExists(msg, "heartbeat") )
            {
                local.wsInfo = websocketInfo(false);
                local.wsInstances = wsInfo.instances;
                local.found = 0;

                for ( var wsI in wsInstances ) 
                {
                    if( structKeyExists(wsI.session.requestParameter, 'uuid'))
                    {
                        try 
                        {
                            local.queryService = new query(datasource = "#Application.datasourceName#", maxrows="1");
                            local.sql = 'SELECT ID, userID
                                        FROM websocket
                                        WHERE token = :token AND userID = :userID AND websocketID = :websocketID';
                            queryService.setSQL(sql);
                            
                            queryService.addParam( name='token', value='#wsI.session.requestParameter.uuid[1]#', cfsqltype='cf_sql_longvarchar');
                            queryService.addParam( name='userID', value='#msg.userID#', cfsqltype='cf_sql_bigint');
                            queryService.addParam( name='websocketID', value='#wsI.session.id#', cfsqltype='cf_sql_longvarchar');
                            
                            local.qFind = queryService.execute().getResult();

                            if( qFind.recordcount > 0)
                            {   
                                //found the userID who sent the message
                                local.queryService = new query(datasource = "#Application.datasourceName#");
                                local.sql = 'UPDATE users
                                            SET activeCon = :activeCon
                                            WHERE ID = :ID';
                                queryService.setSQL(sql);
                                
                                queryService.addParam( name='activeCon', value='#Now()#', cfsqltype='cf_sql_datetime');
                                queryService.addParam( name='ID', value='#qFind.userID#', cfsqltype='cf_sql_bigint');                                    
                                local.qUpdate = queryService.execute();

                                //update the websocket time too
                                local.queryService = new query(datasource = "#Application.datasourceName#");
                                local.sql = 'UPDATE websocket
                                            SET lastModified = :webService
                                            WHERE ID = :ID';
                                queryService.setSQL(sql);
                                
                                queryService.addParam( name='webService', value='#Now()#', cfsqltype='cf_sql_datetime');
                                queryService.addParam( name='ID', value='#qFind.ID#', cfsqltype='cf_sql_bigint');
                                
                                local.qUpdate = queryService.execute();
                                local.dt = {};
                                dt.type = 0;
                                dt.heartbeat = 1;
dt.socketID = wsI.session.id;
dt.uuid = wsI.session.requestParameter.uuid[1];
                                arguments.wsclient.send( serializeJSON(dt) );
                                found++;
                                break;
                            }
                        }
                        catch(any e)
                        {
                            //close connection, there is an error
                            arguments.wsclient.send( e.Message );
                            arguments.wsclient.close();
                        }
                    }
                    else if(structKeyExists(wsI.session.requestParameter, 'ses'))
                    {
                        try 
                        {
                            local.queryService = new query(datasource = "#Application.datasourceName#", maxrows="1");
                            local.sql = 'SELECT ID, userID
                                        FROM websocket
                                        WHERE token = :token AND userID = :userID';
                            queryService.setSQL(sql);
                            
                            queryService.addParam( name='token', value='#wsI.session.requestParameter.ses[1]#', cfsqltype='cf_sql_longvarchar');
                            queryService.addParam( name='userID', value='#msg.userID#', cfsqltype='cf_sql_bigint');
                            
                            local.qFind = queryService.execute().getResult();

                            if( qFind.recordcount > 0)
                            {   
                                //found the userID who sent the message
                                local.queryService = new query(datasource = "#Application.datasourceName#");
                                local.sql = 'UPDATE users
                                            SET activeCon = :activeCon
                                            WHERE ID = :ID';
                                queryService.setSQL(sql);
                                
                                queryService.addParam( name='activeCon', value='#Now()#', cfsqltype='cf_sql_datetime');
                                queryService.addParam( name='ID', value='#qFind.userID#', cfsqltype='cf_sql_bigint');                                    
                                local.qUpdate = queryService.execute();

                                //update the websocket time too
                                local.queryService = new query(datasource = "#Application.datasourceName#");
                                local.sql = 'UPDATE websocket
                                            SET lastModified = :webService
                                            WHERE ID = :ID';
                                queryService.setSQL(sql);
                                
                                queryService.addParam( name='webService', value='#Now()#', cfsqltype='cf_sql_datetime');
                                queryService.addParam( name='ID', value='#qFind.ID#', cfsqltype='cf_sql_bigint');
                                
                                local.qUpdate = queryService.execute();
                                local.dt = {};
                                dt.type = 0;
                                dt.heartbeat = 1;
                                arguments.wsclient.send( serializeJSON(dt) );
                                found++;
                                break;
                            }
                        }
                        catch(any e)
                        {
                            //close connection, there is an error
                            arguments.wsclient.send( e.Message );
                            arguments.wsclient.close();
                        }
                        
                    }
                    else 
                    {
                        //close connection, something is wrong
                        arguments.wsclient.close();
                    }
                }

                if( found == 0)
                {
                   arguments.wsclient.close(); 
                }
            }            
        }
        <!------>
        local.dt = {};
        return serializeJSON(dt);

    }

    function onClose( wsclient, reasonPhrase )
    {
        var userId = getUserIdFromRequest();
        structDelete( static.clientsByUser, userId );
        structDelete( static.rolesByUser, userId );


        //delete specfic websocket from db because a new one will be started on refresh/close
        local.wsInfo = websocketInfo(false);
        local.wsInstances = wsInfo.instances;

        for ( var wsI in wsInstances ) 
        {
            //wsI.session has all info
            try 
            {
                local.queryService = new query(datasource = "#Application.datasourceName#", maxrows="1");
                local.sql = 'DELETE FROM websocket
                            WHERE websocketID = :webID AND userID = :userID';
                queryService.setSQL(sql);
                
                queryService.addParam( name='webID', value='#wsI.session.id#', cfsqltype='cf_sql_longvarchar');
                queryService.addParam( name='userID', value='#msg.userID#', cfsqltype='cf_sql_bigint');
                
                local.qClose = queryService.execute().getResult();

            }
            catch(any e)
            {
                //failed
                arguments.wsclient.send( e.Message );
            }
            
        }
    }

    function onError( wsclient, cfcatch )
    {

    }

    public static function onLastClose()
    {

    }

    // --- static helpers callable from anywhere in the app ---
    public static boolean function sendToUser( required string websocketID, required any message ) 
    {
        if ( !structKeyExists( static.clientsByUser, arguments.websocketID ) )
            {
                try 
                {
                    //not here, remove from DB
                    local.queryService = new query(datasource = static.dsName);
                    local.sql = 'DELETE FROM websocket
                                WHERE websocketID = :websocketID';
                    queryService.setSQL(sql);
                    
                    queryService.addParam( name='websocketID', value='#arguments.websocketID#', cfsqltype='cf_sql_longvarchar');
                    
                    local.qDel = queryService.execute();
                }
                catch(any e)
                {
                    //fail silently
                }
            }
            else
            {
                var cl = static.clientsByUser[ arguments.websocketID ];
                if ( !cl.isOpen() ) 
                {
                    try 
                    {
                        structDelete( static.clientsByUser, arguments.websocketID );
                        local.queryService = new query(datasource = static.dsName);
                        local.sql = 'DELETE FROM websocket
                                    WHERE websocketID = :websocketID';
                        queryService.setSQL(sql);
                        
                        queryService.addParam( name='websocketID', value='#arguments.websocketID#', cfsqltype='cf_sql_longvarchar');
                        
                        local.qDel = queryService.execute();
                    }
                    catch(any e)
                    {
                        //fail silently
                    }
                }
                else
                {
                    cl.send( arguments.message );            
                }
            }

        //collect all the socketIDs to send a message too
        /*
        local.queryService = new query(datasource = static.dsName);
        local.sql = 'SELECT ID, websocketID
                    FROM websocket
                    WHERE userID = :userID AND websocketService = :socketType';
        queryService.setSQL(sql);
        
        queryService.addParam( name='userID', value='#arguments.userID#', cfsqltype='cf_sql_bigint');
        queryService.addParam( name='socketType', value='#arguments.socketType#', cfsqltype='cf_sql_longvarchar');
        
        local.qSockets = queryService.execute().getResult();

        cfloop( query="qSockets" )
        {

            if ( !structKeyExists( static.clientsByUser, websocketID ) )
            {
                try 
                {
                    //not here, remove from DB
                    local.queryService = new query(datasource = static.dsName);
                    local.sql = 'DELETE FROM websocket
                                WHERE ID = :ID';
                    queryService.setSQL(sql);
                    
                    queryService.addParam( name='ID', value='#ID#', cfsqltype='cf_sql_bigint');
                    
                    local.qDel = queryService.execute();
                }
                catch(any e)
                {
                    //fail silently
                }
            }
            else
            {
                var cl = static.clientsByUser[ websocketID ];
                if ( !cl.isOpen() ) 
                {
                    try 
                    {
                        structDelete( static.clientsByUser, websocketID );
                        //not here, remove from DB
                        local.queryService = new query(datasource = static.dsName);
                        local.sql = 'DELETE FROM websocket
                                    WHERE ID = :ID';
                        queryService.setSQL(sql);
                        
                        queryService.addParam( name='ID', value='#ID#', cfsqltype='cf_sql_bigint');
                        
                        local.qDel = queryService.execute();
                    }
                    catch(any e)
                    {
                        //fail silently
                    }
                }
                else
                {
                    cl.send( arguments.message );            
                }
            }
        }
        */
        return true;
    }


    public static void function sendToRole( required string role, required any message ) 
    {
        for ( var userId in static.clientsByUser ) {
            if ( arrayFind( static.rolesByUser[ userId ], arguments.role ) && static.clientsByUser[ userId ].isOpen() )
                static.clientsByUser[ userId ].send( arguments.message );
        }
    }


    // --- your auth integration ---
    private string function getUserIdFromRequest() 
    {
        //get the cgi request to see who is requesting a socket connection
        local.cgiAr = ListToArray(cgi.QUERY_STRING,'&');

        if( len(local.cgiAr) != 2)
        {
            return 0;
        }

        local.uuid = cgiAr[1].ListLast( "=" );
        local.userID = cgiAr[2].ListLast("=");

        local.wsInfo = websocketInfo(false);
        local.wsInstances = wsInfo.instances;

        for ( var wsI in wsInstances ) 
        {


            if( !structKeyExists(wsI.session.requestParameter, "uuid") )
            {
                return 0;
            }

            if( !structKeyExists(wsI.session.requestParameter, "userID") )
            {
                return 0;
            }

            local.socketUUID = wsI.session.requestParameter.uuid[1];
            local.socketUserID = wsI.session.requestParameter.userID[1];


            if( local.socketUUID == local.uuid && local.socketUserID == local.userID )
            {
                try 
                {
                    local.queryService = new query(datasource = "#Application.datasourceName#", maxrows="1");
                    local.sql = 'SELECT ID, userID
                                FROM websocket
                                WHERE token = :token AND websocketID = 0 AND userID = :userID';
                    queryService.setSQL(sql);
                    
                    queryService.addParam( name='token', value='#local.uuid#', cfsqltype='cf_sql_longvarchar');
                    queryService.addParam( name='userID', value='#local.userID#', cfsqltype='cf_sql_bigint');
                    
                    local.qFind = queryService.execute().getResult();

                    if( qFind.recordcount > 0 )
                    {
                        local.queryService = new query(datasource = "#Application.datasourceName#");
                        local.sql = 'UPDATE websocket
                                    SET websocketID = :webID
                                    WHERE ID = :ID';
                        queryService.setSQL(sql);
                        
                        queryService.addParam( name='webID', value='#wsI.session.id#', cfsqltype='cf_sql_longvarchar');
                        queryService.addParam( name='ID', value='#qFind.ID#', cfsqltype='cf_sql_longvarchar');
                        
                        local.qUpdate = queryService.execute();

                        return wsI.session.id;
                    }
                }
                catch(any e)
                {
                    //failed
                    return 0;
                }
            }
        }
        
        return 0;
    }

    private array function getRolesForUser( required string userId ) 
    {
        // add your business logic here — look up roles for this user from your
        // database, auth provider, etc.
        return [ "user" ];
    }
}

And the socket connector .cfm

	<cfset sesID = "" />
	<cfset thisUUID = CreateUUID() />

	<cfif structKeyExists(Session, "readingObj")>
		<cfset sesID = Session.readingObj.getReadingSessionId() />
	</cfif>
<cfdump var="#sesID#" />
	<cfif sesID NEQ "">
		<cfset thisUUID = sesID />
	</cfif>
	
	<cfscript>
		queryService = new query(datasource = GetApplicationSettings().defaultdatasource);
		sql = 'INSERT INTO websocket
		          ( userID, token, websocketService, websocketID )
		          VALUE
		          ( :userID, :token, :msg, 0)';
		queryService.setSQL(sql);

		queryService.addParam( name='userID', value='#Session.sessionObj.getUserId()#', cfsqltype='cf_sql_bigint');
		queryService.addParam( name='token', value='#thisUUID#', cfsqltype='cf_sql_varchar');
		queryService.addParam( name='msg', value='GENERAL', cfsqltype='cf_sql_varchar');

		qWeb = queryService.execute();
	</cfscript>
<cfdump var="#thisUUID#" />
	<script type="text/javascript">
console.log('open socket');
		_socket = new WebSocket("wss://<cfoutput>#Application.websocket#/ws/websocket?uuid=#thisUUID#&userID=#Session.sessionObj.getUserId()#</cfoutput>");
		var _runTimer;

		_socket.onopen = function(evt) 
		{
console.log("Connected");
			stayActive();
			//start an interal to send a ping every 1 minute to maintain heartbeat
		};

		function stayActive()
		{
			_runTimer = setInterval(stayConnected, 5000);// was 60000
		}


		function stayConnected()
		{
			console.log('stayConnected Called from somewhere!');
			if(_socket)
			{
				let o = {}
				o.heartbeat = 1;
				o.userID = _socketUserID;

				if( o.userID != 0)
				{
					_socket.send(JSON.stringify(o));
				}
			}
		}

		if(typeof _socket !== 'undefined')
		{
			_awaitingAnswer = 0;
			_socket.onmessage = function(event) 
			{
console.log("Received onMessage:", event.data);
				//console.log( JSON.parse(event.data) );
				let resp = JSON.parse(event.data);

				if( resp.type == 0)
				{
					//heartbeat response
				}
			};

			_socket.onclose = function(evt) 
			{
				console.log('closed me off now');
				console.log(evt);
				if(typeof(runTimer) !== 'undefined') 
				{
					console.log('inverval should be cleared')
					clearInterval(runTimer);
				}
				console.log("Connection closed");
			};

			_socket.onerror = function(error)
			{
			  console.error("WebSocket error:", error);
			};
		}
</script>

The above code is used as an include on all pages, so it is the same code that runs everytime a page loads.

The reason the heartbeat is there is to keep the connection active, otherwise the browsers disconnect after a few minutes. Disabling it only causes the connection to close at some point. If I turn it off and use the form on the page to send a message to a user gives the same problem, the old socket is used, but eventually it does become the right socket.