Crashes on WebGL with Cloud Code calls (_WebSocketFree)

Hello there !

We are currently working on a WebGL game that relies a lot on Cloud Code.

On WebGL, we have been systematically experiencing the same crash in the authentication steps of our game :

Uncaught TypeError: Cannot read properties of undefined (reading 'readyState')
    at _WebSocketFree (test_lobah.framework.js:8088:45)
    at invoke_vi (test_lobah.framework.js:18587:29)
    at WebSocketFactory_HandleInstanceDestroy_mB533D21CA7D36EDF0D7D4FD279948195E8A39534 (test_lobah.wasm:0x3570a2c)
    at invoke_vii (test_lobah.framework.js:18576:29)
    at WebSocket_Finalize_m4CCA49F0C6CB66F0B3435546A16A14424F53E429 (test_lobah.wasm:0x357069f)
    at RuntimeInvoker_TrueVoid_t4861ACF8F4594C3437BB48B6E56783494B843915(:61754/void (*)(), MethodInfo const*, void*, void**, void*) (http://localhost:61754/Build/test_lobah.wasm)
    at il2cpp::vm::Runtime::InvokeWithThrow(:61754/MethodInfo const*, void*, void**) (http://localhost:61754/Build/test_lobah.wasm)
    at invoke_iiii (test_lobah.framework.js:18631:36)
    at il2cpp::vm::Runtime::Invoke(:61754/MethodInfo const*, void*, void**, Il2CppException**) (http://localhost:61754/Build/test_lobah.wasm)
    at il2cpp::gc::GarbageCollector::RunFinalizer(:61754/void*, void*) (http://localhost:61754/Build/test_lobah.wasm)
    at GC_invoke_finalizers (test_lobah.wasm:0x9a223)
    at il2cpp::gc::GarbageCollector::CollectALittle(:61754/) (http://localhost:61754/Build/test_lobah.wasm)
    at il2cpp_gc_collect_a_little (test_lobah.wasm:0xee475)
    at scripting_gc_collect_a_little(:61754/) (http://localhost:61754/Build/test_lobah.wasm)
    at MainLoop(:61754/) (http://localhost:61754/Build/test_lobah.wasm)
    at _JS_CallAsLongAsNoExceptionsSeen (test_lobah.framework.js:1952:28)
    at UpdateUnityFrame(:61754/) (http://localhost:61754/Build/test_lobah.wasm)
    at callUserCallback (test_lobah.framework.js:5539:9)
    at Object.runIter (test_lobah.framework.js:5595:11)
    at Browser_mainLoop_runner (test_lobah.framework.js:6233:26)

After some investigation, we managed to isolate the issue. This happens after calling one of our cloud code modules.

We looked into the generated js code and enabled debugging for this WebSocket API, to get some logs.

What we believe happens is that Cloud Code opens a WebSocket, then closes the WebSocket when it’s done.
After a short time, the WebSocket is garbage collected and _WebSocketFree is called. This free function checks if the web socket is closed before deleting it (for safety reasons I suppose, if for whatever reason it wasn’t closed before being garbage collected or freed).

function _WebSocketFree(instanceId) {
  
  		var instance = webSocketState.instances[instanceId];
  
  		if (!instance) return 0;

      console.log(`[JSLIB WebSocket] ${JSON.stringify(instance)}`);
  
  		// Close if not closed
  		if (instance.ws !== null && instance.ws.readyState < 2)
  			instance.ws.close();
  
  		// Remove reference
  		delete webSocketState.instances[instanceId];
  
  		return 0;
  
  	}

When we log the content of the instance, we find out that the actual websocket (instance.ws) is undefined (which makes sense, since the web socket was already explicitly closed). This seems to be the cause of the crash - _WebSocketFree checks instance.ws for null, but doesn’t check if it’s undefined.

Our hotfix, until this gets properly resolved in a future update :

function _WebSocketFree(instanceId) {
  
  		var instance = webSocketState.instances[instanceId];
  
  		if (!instance) return 0;

      console.log(`[JSLIB WebSocket] ${JSON.stringify(instance)}`);
  
  		// Close if not closed
  		if (instance.ws !== null && instance.ws !== undefined && instance.ws.readyState < 2)
  			instance.ws.close();
  
  		// Remove reference
  		delete webSocketState.instances[instanceId];
  
  		return 0;
  
  	}

Now, messing with the generated js code is not ideal and we are not aware of all potential side effects.

Also, this forces us to apply the fix manually after every build.

We are launching our game in a couple weeks, so I guess we will have to stick to that solution for the time being. It would be great if this fix could be shipped in the next update for the relevant package.

PS : enabling WebAssembly tables is also an issue with that bit in particular, since there are functions related to that WebSocket conundrum that seem to still be called via the legacy Emscripten dynCall.

To make it work with WebAssembly tables enabled, we applied another fix that is explained in that thread (remapping via a .jspre plugin) : makeDynCall replacing dynCall_ in Unity 6?

I think you can create a JSLIB file of your own, and have there a method named WebSocketFree.
That should override the generated _WebSocketFree.
And then you won’t need to manually override this method on every build.

I’m not sure if it’s a method from a package or the editor. If it’s in a package, you can duplicate the package and override it in the source.

If it’s from the editor, you can override it in the editor web files, but then you’ll have to do that in every machine you use to compile (will be difficult on cloud builds).

I think that first option is the cleanest.

Hello,

The respective team is currently working on a fix for the WebSocket ws undefined unhandled exception and it will be included in the upcoming release.

Please let me know if there are any questions!

similar issue happens for me on this method (line 7):

function _WebSocketSend(instanceId, bufferPtr, length) {
	var instance = webSocketState.instances[instanceId];
	if (!instance)
		return -1;
	if (instance.ws === null)
		return -3;
	if (instance.ws.readyState !== 1)
		return -6;
	instance.ws.send(HEAPU8.buffer.slice(bufferPtr, bufferPtr + length));
	return 0
}

I just wanted to add that I’ve managed to work around this issue by implementing a custom post-build script that decompresses the play.framework.js.unityweb file, replaces all ===null and !==null checks with the more resilient ==null and !=null (to also catch undefined), and then recompresses the file before deployment.

Here’s the script I’m using:
(see full code snippet below)

While this workaround reliably prevents the crash in our case, it’s still a manual patch and not an ideal long-term solution. A proper fix on Unity’s side would of course be much more sustainable and future-proof.

What really surprises me is that such a critical and easily reproducible issue - affecting any WebGL game using Unity Push Messages, Relay, or WebSocket-based networking - has not been addressed officially yet. In its current state, Unity WebGL builds relying on persistent WebSocket connections are unreliable in production. Even switching browser tabs or normal gameplay can eventually trigger this crash.

Hopefully this workaround helps others until a proper fix is implemented.

Code:

using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using System.IO;
using System.IO.Compression;

public class FrameworkPatchPostBuild : IPostprocessBuildWithReport
{
    public int callbackOrder => 0;

    public void OnPostprocessBuild(BuildReport report)
    {
        // ensure the build target is WebGL
        if (report.summary.platform != BuildTarget.WebGL)
            return;

        // path to the play.framework.js.unityweb file
        string buildPath = report.summary.outputPath + "/Build";
        string frameworkPath = buildPath + "/play.framework.js.unityweb";

        if (!File.Exists(frameworkPath))
        {
            UnityEngine.Debug.LogWarning("[PostBuildPatch] play.framework.js.unityweb not found.");
            UnityEngine.Debug.LogWarning(buildPath);
            UnityEngine.Debug.LogWarning(frameworkPath);
            return;
        }

        // temporary path for the extracted file
        string tempExtractedPath = Path.Combine(buildPath, "play.framework.js");

        // decompress the .gz file
        File.Move(frameworkPath, frameworkPath + ".gz");
        DecompressGzip(frameworkPath + ".gz", tempExtractedPath);

        // apply patch to the extracted file
        string content = File.ReadAllText(tempExtractedPath);
        string patchedContent = content.Replace("!==null", "!=null").Replace("===null", "==null");
        File.WriteAllText(tempExtractedPath, patchedContent);

        // recompress the patched file
        CompressGzip(tempExtractedPath, frameworkPath);

        // clean up temporary files
        File.Delete(tempExtractedPath);
        File.Delete(frameworkPath + ".gz");

        UnityEngine.Debug.Log("[PostBuildPatch] play.framework.js.unityweb was patched and restored.");
    }

    private void DecompressGzip(string gzipPath, string outputPath)
    {
        using (FileStream originalFileStream = new FileStream(gzipPath, FileMode.Open, FileAccess.Read))
        using (FileStream decompressedFileStream = new FileStream(outputPath, FileMode.Create))
        using (GZipStream decompressionStream = new GZipStream(originalFileStream, CompressionMode.Decompress))
        {
            decompressionStream.CopyTo(decompressedFileStream);
        }
    }

    private void CompressGzip(string inputPath, string gzipPath)
    {
        using (FileStream originalFileStream = new FileStream(inputPath, FileMode.Open, FileAccess.Read))
        using (FileStream compressedFileStream = new FileStream(gzipPath, FileMode.Create))
        using (GZipStream compressionStream = new GZipStream(compressedFileStream, CompressionLevel.Optimal))
        {
            originalFileStream.CopyTo(compressionStream);
        }
    }
}