How to safely invoke the client (revamped)

You have probably seen my old tutorial about this. The code was pretty garbage. So I wanted to revamp the tutorial.

So, You have probably heard this saying ATLEAST once: “Dont invoke the client”
Now its true, Because they can hook the remote function and whenever the server invokes it the server script will infinitely yield.
How would they do this?

RemoteFunction.OnClientInvoke = function() task.wait(9e9) end

Yep. As simple as that. And your whole server script will just wait for that stupid local script to respond (hint: it will never)

What do we do in this case? We make the script give it only X seconds to respond, If it will not respond, It will just ignore the request. How do we do this?

First, we wrap the invoke function in a task.spawn.

function safeInvokeClient(remoteFunction, timeout, ...)
   local result = nil

   task.spawn(function()
      result = remoteFunction:InvokeClient(...)
   end)
end

Next, We will add a for loop waiting for the request.

function safeInvokeClient(remoteFunction, timeout, ...)
   local result = nil

   task.spawn(function()
      result = remoteFunction:InvokeClient(...)
   end)
   for index = 1, timeout do
      if result ~= nil then
         break
      end
      task.wait(1)
   end
   return if result ~= nil then result else nil
end

And the if block was to check if the request was there or not. Its as simple as that!

We can call the function like this:

safeInvokeClient(RemoteFunction, 5, player, "hi")

Now, the code will give it 5 seconds for the client to return the request.
But this does not make it completely safe! Make sure you check the type of the result too. You can use t for this.

Feedbacks welcome.

11 Likes

Is the second if statement necessary? You’re just returning nil if result is nil. If there’s a reason behind this it’d be nice to know.

3 Likes

Yes. Thats to check if the request has been recieved or not.

edit: wait you are right. lemme edit it rq.

2 Likes

But what is the point of invoking the client? The only thing you would really do that for is getting a is getting a local value, but thats just plain dangerous.

4 Likes

Minor Note: if anyone is looking for the old topic, here it is:


Great tutorial tho, Really appreciate it

1 Like

I wouldn’t even use RemoteFunction:InvokeClient(). Not only is there the yielding problem, but there’s also the problem that it may error. Here’s an alternative using remote events. I haven’t tested it but I believe it’ll work with some edits if it doesn’t work yet.

Server code (in a module script):

local Players = game:GetService("Players")

local InvokeClientServerModule = {}

InvokeClientServerModule.timeoutReturnValue = setmetatable({}, {tostring = function() return "TIMEOUT_RETURN_VALUE" end})
InvokeClientServerModule.playerLeftReturnValue = setmetatable({}, {tostring = function() return "PLAYER_LEFT_RETURN_VALUE" end})
InvokeClientServerModule.successReturnValue = setmetatable({}, {tostring = function() return "SUCCESS_RETURN_VALUE" end})

local data = {}

local function resumeCoroutine(playerRemoteEventData, invokeNumber, coroutineToResume, ...)
	playerRemoteData.howManyReceivedOrTimedOut += 1
	playerRemoteData.waitingCoroutineForEachInvoke[invokeNumber] = nil
	if playerRemoteData.howManyReceivedOrTimedOut == playerRemoteData.howManySent then
		-- This is done so that there's no unnecessary event connection.
		playerRemoteData.connection:Disconnect()
		playerRemoteData.connection = nil
	end
	coroutine.resume(coroutineToResume, ...)
end

local function doesPlayerRemoteEventDataExist(player, remoteEvent)
	local dataForPlayer = data[player]
	if dataForPlayer == nil then
		return false
	end
	return dataForPlayer[remoteEvent] ~= nil
end

local function resumeCoroutineIfTimedOut(player, remoteEvent, invokeNumber)
	if not doesPlayerRemoteDataExist(player, remoteEvent) then
		-- Player has left the game before timeout.
		return
	end
	local playerRemoteData = data[player][remoteEvent]
	local waitingCoroutine = playerRemoteData.waitingCoroutineForEachInvoke[invokeNumber]
	if waitingCoroutine == nil then
		-- The client has sent a value before timeout.
		return
	end
	-- InvokeClientServerModule.invoke will now return nil because no value was received from the client before timeout.
	resumeCoroutine(playerRemoteEventData, invokeNumber, waitingCoroutine, InvokeClientServerModule.timeoutReturnValue)
end

local function handleOnServerEvent(player, remoteEvent, invokeNumber, ...)
	if not doesPlayerRemoteDataExist(player, remoteEvent) then
		-- exploiter or mistake in code (or maybe the player can have left before than the sent data was received by the server)
		return
	end
	if invokeNumber > playerRemoteData.howManySent then
		-- exploiter or mistake in code
	end
	local waitingCoroutine = playerRemoteData.waitingCoroutineForEachInvoke[invokeNumber]
	if waitingCoroutine == nil then
		-- Timeout has been reached or the client fired the remote twice with the same invoke number because of either a mistake in code or an exploiter.
		return
	end
	resumeCoroutine(playerRemoteEventData, invokeNumber, waitingCoroutine, InvokeClientServerModule.successReturnValue, ...)
end

local function setupPlayerRemoteEventDataIfNecessary(player, remoteEvent)
	local dataForPlayer = data[player]
	if dataForPlayer == nil then
		dataForPlayer = {}
		data[player] = dataForPlayer
	end
	local playerRemoteData = dataForPlayer[remoteEvent]
	if playerRemoteData == nil then
		playerRemoteData = {
			howManySent = 0
			howManyReceivedOrTimedOut = 0
			waitingCoroutineForEachInvoke = {}
		}
		dataForPlayer[remoteEvent] = playerRemoteData
	end
end

function InvokeClientServerModule.invoke(player, remoteEvent, timeout, ...)
	setupPlayerRemoteEventDataIfNecessary(player, remoteEvent)
	local playerRemoteEventData = data[player][remoteEvent]
	local invokeNumber = playerRemoteEventData.howManySent + 1
	playerRemoteEventData.howManySent = invokeNumber
	remoteEvent:FireClient(player, invokeNumber, ...)
	if playerRemoteEventData.connection == nil then
		-- There were no invokes waiting for response before this one was made so there was no connection and thus a new connection needs to be made.
		playerRemoteEventData.connection = remoteEvent.OnServerEvent:Connect(function(player, invokeNumber, ...)
			handleOnServerEvent(player, remoteEvent, invokeNumber, ...)
		end)
	end
	playerRemoteEventData.waitingCoroutineForEachInvoke[invokeNumber] = coroutine.running()
	task.delay(timeout, resumeCoroutineIfTimedOut, player, remoteEvent, invokeNumber)
	return coroutine.yield()
end

local function onPlayerRemoving(player)
	local playerData = data[player]
	if playerData == nil then
		-- This player hasn't been invoked at all.
		return
	end
	data[player] = nil
	for _, playerRemoteData in playerData do
		for invokeNumber, waitingCoroutine in waitingCoroutineForEachInvoke do
			resumeCoroutine(playerRemoteEventData, invokeNumber, waitingCoroutine, InvokeClientServerModule.playerLeftReturnValue)
		end
	end
end

Players.PlayerRemoving:Connect(onPlayerRemoving)

return InvokeClientServerModule

The client function that is connected to RemoteEvent.OnClientEvent should have invokeNumber as its first parameter. and the client should give invoke number as the first argument to FireServer.

local function handleClientInvoke(invokeNumber, --[[possibly other parameters after invokeNumber]])
	--[[
	do something
	--]]
	remoteEvent:FireServer(invokeNumber, --[[possibly other arguments after invokeNumber]])
end
remoteEvent.OnClientEvent:Connect(handleClientInvoke)

Edit June 18:
I realised I had defined timeoutReturnValue and playerLeftReturnValue as local variables. They must be stored as member variables of InvokeClientServerModule so that other modulescripts or scripts using the function can check the reason for resuming the thread. I’ve fixed this now.

I also added InvokeClientServerModule.successReturnValue because I realised that it’s probably a better idea that the first value returned by InvokeClientServerModule.invoke is always a value that explicitly tells the reason for continuing the thread. With the earlier code, the first return value was either

  • timeoutReturnValue,
  • playerLeftReturnValue or
  • the first one of the values received from the client.

With the current code, the first value is either

  • InvokeClientServerModule.timeoutReturnValue,
  • InvokeClientServerModule.playerLeftReturnValue or
  • InvokeClientServerModule.successReturnValue.

In the case of the first return value being InvokeClientServerModule.successReturnValue, the other return values are values received from the client. In the other two cases, there are no other return values.

1 Like

You will fall to the same problem again; you just created a remote function but in a much more unnessecary way.

5 Likes

What do you mean by “You will fall to the same problem again”?

My code will resume the thread (coroutine) after the given timeout time if the client hasn’t fired the remote yet. So infinite yield shouldn’t be a problem.

My code will also not error if the player leaves. It will continue the thread in that case as well.

And the value returned by InvokeClientServerModule.invoke can be checked to see whether the thread was continued because of

  • player leaving (playerLeftReturnValue InvokeClientServerModule.playerLeftReturnValue),
  • timeout (timeoutReturnValue InvokeClientServerModule.timeoutReturnValue) or
  • server receiving the value from the client in time (neither of the above return values InvokeClientServerModule.successReturnValue).

I haven’t tested the code, but it should work in the way I described above.

1 Like

My thing will also not error due to the invoke being wrapped in a spawn function.

2 Likes

I’m pretty sure it will error although the error will not stop the code in safeInvokeClient. Although the error won’t affect the behavior of the code, I still think it’s better to use code that does not error. But that’s a matter of opinion I guess.

I know there are situations where you can’t prevent errors (many API functions may error). In this kind of situation, pcall should be used (not task.spawn). But I would not use even pcall in a situation where I can prevent the code from erroring.

1 Like

I did not use task.spawn for preventing the code from erroring, just the infinite yield. But it solved the two issues I guess.

I agree.

1 Like

Its plain dangerous if you dont create a secure system. You should use sanity checks so that even if the client sent the wrong spoofed details, It wont affect it so much.

2 Likes

map voting that is based on player ui?

1 Like

task.spawn returns a thread, not arguments.

1 Like

You can wrap it like so:

local function invokeClient(player, ...)
   local result
   task.spawn(function()
      result = remoteFunction:InvokeClient(player, ...)
   end)
   ...
end

I do however, advise to use remote events—as it is (probably) faster and probably less prone to errors or long yields.

1 Like

Done. Forgot about that.

1 Like

sorry for the necropost but you cant seem to use varargs for invokeclient anymore
image

1 Like

i know that this might seem like a super tacky solution, but i would try doing the following:

local InTable = table.create(...)
RemoteFunction:InvokeClient(table.unpack(InTable))

pls test it out and lmk if it works!

1 Like

Could you show your full implementation?

1 Like

i just copied your example script into a blank script, and it spouted that error