How to intercept all server calls, severside

I’d like a way to do some very generic stuff before server calls are executed. Something like:

// client
Meteor.call("someFunc", 1, 2, 3, function(err, result) {
    // this callback is only executed if the server call was executed
});

// server
Meteor.methods({
    someFunc : function(arg1, arg2, arg3) {
        // do some funky stuff
    }
});

// func would be someFunc, and args would be [1, 2, 3]
Meteor.onServerCall(function(func, args) {
    // code to be executed before 
    if (args[0] == "feeling cooperative") {
        func.apply(args);
    } else {
        console.log("server call not executed");
    }
});

I’ve done something like this in the past to log all errors throw by Meteor methods. The basic idea is to define your methods in an object:

const methods = {
    someFunc(arg1, arg2, args) {
        ....
    }
};

And then wrap them in a decorator function before passing them into Meteor.methods. In my case, exceptionLogger wraps each function in the methods map with a decorator function:

Meteor.methods(exceptionLogger(methods));

And exceptionLogger looks something like this. Feel free to substitute your own logic…

function exceptionLogger(methods) {
    return _.reduce(Object.keys(methods), function(newMethods, method) {
        newMethods[method] = function() {
            try {
                return methods[method].apply(this, arguments);
            }
            catch (e) {
                logger.error(e);
                throw e;
            }
        }
        return newMethods;
    }, {});
}

It’s worth noting that adding business logic to decorators can quickly get out of hand and confusing. I’d think carefully about more explicit ways of doing what you’re trying to do before going this route.

1 Like

Hi,

That is indeed a clever solution. What I wound up doing was more of a hack, but it works. Adding the audit-argument-checks package causes Match._failIfArgumentsAreNotAllChecked to be called before any server method invocation. So, I redefine this function serverside, like this:

Meteor.startup(function() {

    // attempt to avoid multiple identical calls to server functions
    var serverCallIds = [];
    Match._failIfArgumentsAreNotAllChecked = function(f, context, args, description) {
        if (context.isSimulation == true || context.isSimulation == false) {
            // this indicates a server call (not a publish call)
            if (args.length > 0 && typeof args[0].serverCallId != "undefined") {
                var serverCallId = args[0].serverCallId;
                if (serverCallIds.includes(serverCallId)) { // check serverCallId
                    throw new Meteor.Error("already called");
                } else {
                    serverCallIds.push(serverCallId);
                    while (serverCallIds.length > 1000) {
                        serverCallIds.splice(0, 1);
                    }
                    args.splice(0, 1);
                }
            }
        }
        console.log("execute server call");
        return f.apply(context, args);
    };

});

Then I call all server method through my own serverCall on the client side, like this:


	serverCall : function(functionName, args) {
		var callback = typeof arguments[arguments.length - 1] == "function" ? arguments[arguments.length - 1] : null;
		var functionArgs = [ functionName, {
			serverCallId : functionName + "-" + Math.floor(Math.random() * 10000000000000000)
		} ];
		for ( var i in arguments) {
			if (i >= 1 && i < arguments.length - (callback ? 1 : 0)) {
				functionArgs.push(arguments[i]);
			}
		}
		functionArgs.push(function(err, result) {
			if (err) {
				if (err.error == "already called") {
					RavenLogger.log("avoided redundant call to " + functionName);
				} else {
					// show an error message
					RavenLogger.log(new Error(err));
					Materialize.toast(TAPi18n.__("server_error") + " : " + err, 4000);
				}
			} else if (callback) {
				callback(result);
			}
		});
		Meteor.call.apply(this, functionArgs);
	},

The point of all this is to avoid redundant calls to the same function. I had tried by using Meteor.apply with the noRetry flag, like this:

	serverCallNoRetry : function(functionName, args) {
		var callback = typeof arguments[arguments.length - 1] == "function" ? arguments[arguments.length - 1] : null;
		var functionArgs = [ functionName ];
		var functionArgsArr = [];
		for ( var i in arguments) {
			if (i >= 1 && i < arguments.length - (callback ? 1 : 0)) {
				functionArgsArr.push(arguments[i]);
			}
		}
		functionArgs.push(functionArgsArr);

		functionArgs.push({
			noRetry : true
		});
		functionArgs.push(function(err, result) {
			if (err) {
				RavenLogger.log(new Error("call to " + functionName + " failed : " + err));
			} else if (callback) {
				callback(result);
			}
		});
		Meteor.apply.apply(this, functionArgs);
	},

… but that rather failed due to the fact that retries are actually quite necessary, it turns out. With noRetry set to true, many server calls just fail completely. So, it seems that what I really need is a way to retry, and yet avoid executing the server code multiple times. In order to accomplish this, I attach a serverCallId to each invocation, which I store and check to make sure that only the first attempt that gets through will be executed.

The issue we are seeing (maybe I should have led with that…) is on mobile devices. When the internet connection is lost, it seems that the client continues trying server calls. When the connection is restored, all these queued requests go through, and they are all executed. If the server call inserts data in Mongo, then we see multiple identical entries, in one case 379 of them!

This effect is hard to produce. Apparently I still don’t understand the circumstances very well, as I can’t make it happen in on the desktop at all, requiring a full mobile build and deployment in order to test… very tedious indeed!

Any thoughts on this dilemma will be much appreciated.