User is sometimes logged out right after reconnecting to the RC server

Description

tl;dr Can anyone think of a reason why the Rocket Chat web app would randomly log a user out when their browser reconnects to the RC server after being offline for a while?

I’m using standard email/password auth and am nowhere near the 90 day login expiration threshold. It’s currently happening around once a day to my users.

==============================

I’ve customized RC to properly install and function as a PWA (with very basic service worker). I have the PWA installed on my Android phone. It mostly works fine except for one problem that I’ve noticed happening quite often:

  1. At night, I put my phone on Airplane mode.
  2. In the morning, I turn Wifi back on.
  3. I open the RC PWA app on my phone
  4. The app opens to the channel that I was in when I last used it (so far so good…)
  5. I see a brief “Connecting…” at the top of the app
  6. I’m immediately logged out of the app with an error message “Invalid User”.
  7. If I try to login again, I just get a blank screen. I then need to fully close the PWA and re-open it to get it working again (equivalent to a browser refresh).

The “Invalid User” error message is coming from the fact that it’s trying to run loadMissedMessages upon reconnection but since the user is already logged out, there’s no value for Meteor.userId() so the loadMissedMessages method on the server is throwing an exception, which is understandable.

The real question is - why is the user sometimes being logged out upon reconnection to the server AND why is it only happening on the PWA? I also can’t reproduce this every time. If I just do a Meteor.disconnect() + Meteor.reconnect() from the browser console on my desktop web app, it reconnects fine and everything is OK. Likewise, if I disconnect the Wifi on my phone for 15 minutes and reconnect it’s usually OK. The problem seems to happen more often after longer periods of disconnection (overnight).

As a next step I’m going to try adding Meteor APM so that I can get more insights into any client-side errors. I’m also going to add more logging to figure out exactly what code gets run during a reconnection to the server (for example, I see a lot of autoruns, some contain things like Meteor.connection.status(). I also see that CachedCollection does a data sync onReconnect as well).

I know that Meteor’s accounts-base package will log you out if it can’t find a login token in localStorage so it’s possible that maybe something is evicting the login token from the storage being used by the PWA? I think PWAs use separate storage container from Chrome / Safari localStorage, but not 100% sure.

Server Setup Information

  • Version of Rocket.Chat Server: 3.7.4
  • Operating System: Ubuntu 18.04
  • Deployment Method: Manual (Rocket.Chat in Ubuntu - Rocket.Chat Docs)
  • Number of Running Instances: 1
  • DB Replicaset Oplog: Enabled
  • NodeJS Version: 12.14.0 - x64
  • MongoDB Version: 4.2.11

SOLVED: So it turns out this didn’t have anything to do with my app being a PWA. It appears that the problem is actually a bug in some generated JS code in the Meteor DDP client. The Meteor source code itself is fine, but when it’s transpiled from ES6 (by Babel, I believe), there’s a bug in how a few lines of code get transpiled.

I’ve just opened a ticket with Meteor here:

Below is my workaround until the issue is reviewed + fixed in Meteor’s DDP client.

NOTE: This workaround takes code from the ddp-client package in Meteor 1.11.1. If you’re using a different version of Meteor you may need different code!

In client/startup/startup.js:

import { hasOwn, isEmpty } from 'meteor/ddp-common/utils';

Then near the top of the Meteor.startup(...) block, override Accounts.connection._livedata_result from the DDP Client as follows to fix the bug:

	// Overriding Accounts.connection._livedata_result from ddp-client to work around the bug of Array.prototype.find
	// not getting properly transpiled in our case.
	Accounts.connection._livedata_result = function(msg) {
		// id, result or error. error has error (code), reason, details

		const self = Accounts.connection;

		// Lets make sure there are no buffered writes before returning result.
		if (! isEmpty(self._bufferedWrites)) {
			self._flushBufferedWrites();
		}

		// find the outstanding request
		// should be O(1) in nearly all realistic use cases
		if (isEmpty(self._outstandingMethodBlocks)) {
			Meteor._debug('Received method result but no methods outstanding');
			return;
		}
		const currentMethodBlock = self._outstandingMethodBlocks[0].methods;
		let i;
		let m;

		// Original implementation using Array.prototype.find() that wasn't getting transpiled correctly...
		/*
			const m = currentMethodBlock.find((method, idx) => {
				const found = method.methodId === msg.id;
				if (found) i = idx;
				return found;
			});
		*/

		// New implementation without using find()...
		for (i = 0; i < currentMethodBlock.length; i++) {
			const currentMethodInvoker = currentMethodBlock[i];
			if (currentMethodInvoker.methodId === msg.id) {
				m = currentMethodInvoker;
				break;
			}
		}

		if (!m) {
			Meteor._debug("Can't match method response to original method call", msg);
			return;
		}

		// Remove from current method block. This may leave the block empty, but we
		// don't move on to the next block until the callback has been delivered, in
		// _outstandingMethodFinished.
		currentMethodBlock.splice(i, 1);

		if (hasOwn.call(msg, 'error')) {
			m.receiveResult(
				new Meteor.Error(msg.error.error, msg.error.reason, msg.error.details),
			);
		} else {
			// msg.result may be undefined if the method didn't return a value
			m.receiveResult(undefined, msg.result);
		}
	};