Cannot catch connection error in WebGL

Hello,

How can I catch connection error exception in WebGL?

When I built and ran the sdk sample code with webgl/chrome, I turned off the nakama local server for testing error handling, and AuthenticateDeviceAsync task never finished.

using System;
using UnityEngine;

namespace Nakama.Snippets
{
    // ReSharper disable once InconsistentNaming
    public class WebGLConnect : MonoBehaviour
    {
        private const string SessionTokenKey = "nksession";
        private const string UdidKey = "udid";

        private IClient _client;
        private ISocket _socket;

        public string serverText;
        public string serverPortText;

        public async void Awake()
        {
            try
            {
                const string scheme = "http";
                string host = serverText;
                int port = Int32.Parse(serverPortText);
                const string serverKey = "defaultkey";

                _client = new Client(scheme, host, port, serverKey, UnityWebRequestAdapter.Instance);
                _socket = _client.NewSocket();
                _socket.Closed += () => Debug.Log("Socket closed.");
                _socket.Connected += () => Debug.Log("Socket connected.");
                _socket.ReceivedError += e => Debug.Log("Socket error: " + e.Message);

                // Cant use SystemInfo.deviceUniqueIdentifier with WebGL builds.
                var udid = PlayerPrefs.GetString(UdidKey, Guid.NewGuid().ToString());
                Debug.Log("Unique Device ID: " + udid);

                ISession session;
                var sessionToken = PlayerPrefs.GetString(SessionTokenKey);
                if (string.IsNullOrEmpty(sessionToken) || (session = Session.Restore(sessionToken)).IsExpired)
                {
                    Debug.Log("@@@@@@@@@@@@@@@ authenticate device start");

                    session = await _client.AuthenticateDeviceAsync(udid);

                    Debug.Log("@@@@@@@@@@@@@@@ authenticate device end");

                    PlayerPrefs.SetString(UdidKey, udid);
                    PlayerPrefs.SetString(SessionTokenKey, session.AuthToken);
                }

                Debug.Log("Session Token: " + session.AuthToken);
                await _socket.ConnectAsync(session, true);
                Debug.Log("Connected ");
                var match = await _socket.CreateMatchAsync();
                Debug.Log("Created match: " + match.Id);

                await _socket.CloseAsync();
            }
            catch (Exception e)
            {
                Debug.LogError(e.ToString());
            }
        }

        private void OnApplicationQuit()
        {
            _socket?.CloseAsync();
        }
    }
}

{Details}

  • Nakama version - 3.22.0
  • Unity SDK version - 3.13.0
  • Unity Editor version - 2022.3.18f1

Hi @steven060594!

Make sure you follow our WebGL compatibility guidelines: GitHub - heroiclabs/nakama-unity: Unity client for Nakama server.

If this still doesn’t work, what is probably happening here is that the requests don’t have a timeout so the client is trying to connect “forever” without ever raising an error, since it’s inheriting the one from the browser, in this case, Chrome has a default idle socket timeout of 300 seconds (5 minutes).

Could you try setting a timeout on the client? After creating the client:

_client.Timeout = 10;

This sets the request timeout to 10 seconds (you can put a higher or lower value if you want). You can see more about that here: Unity/.Net - Heroic Labs Documentation

Let me know if this works, so we can try to debug further.

Hi @DannyIsYog ,

Thank you for your reply!

I set the timeout as you suggested, but the result was the same :cry:

using System;
using UnityEngine;

namespace Nakama.Snippets
{
    // ReSharper disable once InconsistentNaming
    public class WebGLConnect : MonoBehaviour
    {
        private const string SessionTokenKey = "nksession";
        private const string UdidKey = "udid";

        private IClient _client;
        private ISocket _socket;

        public string serverText;
        public string serverPortText;

        public async void Awake()
        {
            try
            {
                const string scheme = "http";
                string host = serverText;
                int port = Int32.Parse(serverPortText);
                const string serverKey = "defaultkey";

                _client = new Client(scheme, host, port, serverKey, UnityWebRequestAdapter.Instance);
                _client.Timeout = 10; //Set timeout
                _socket = _client.NewSocket();
                _socket.Closed += () => Debug.Log("Socket closed.");
                _socket.Connected += () => Debug.Log("Socket connected.");
                _socket.ReceivedError += e => Debug.Log("Socket error: " + e.Message);

                // Cant use SystemInfo.deviceUniqueIdentifier with WebGL builds.
                var udid = PlayerPrefs.GetString(UdidKey, Guid.NewGuid().ToString());
                Debug.Log("Unique Device ID: " + udid);

                ISession session;
                var sessionToken = PlayerPrefs.GetString(SessionTokenKey);
                if (string.IsNullOrEmpty(sessionToken) || (session = Session.Restore(sessionToken)).IsExpired)
                {
                    Debug.Log("@@@@@@@@@@@@@@@ authenticate device start");

                    session = await _client.AuthenticateDeviceAsync(udid);

                    Debug.Log("@@@@@@@@@@@@@@@ authenticate device end");

                    PlayerPrefs.SetString(UdidKey, udid);
                    PlayerPrefs.SetString(SessionTokenKey, session.AuthToken);
                }

                Debug.Log("Session Token: " + session.AuthToken);
                await _socket.ConnectAsync(session, true);
                Debug.Log("Connected ");
                var match = await _socket.CreateMatchAsync();
                Debug.Log("Created match: " + match.Id);

                await _socket.CloseAsync();
            }
            catch (Exception e)
            {
                Debug.LogError("Exception occurred: " + e.ToString());
            }
        }

        private void OnApplicationQuit()
        {
            _socket?.CloseAsync();
        }
    }
}

So I modified SendRequest in UnityWebRequestAdapter to print console log:

private static IEnumerator SendRequest(UnityWebRequest www, Action<string> callback,
    Action<ApiResponseException> errback)
{
    Debug.Log($"@@@@@@@@@@@@@ Sending request: {www.method} {www.url}"); //check the request
    yield return www.SendWebRequest();
    Debug.Log($"@@@@@@@@@@@@@ Response: {www.responseCode} {www.downloadHandler.text}"); //check the response
    if (IsNetworkError(www))
    {
        Debug.Log($"@@@@@@@@@@@@@ Network error: {www.error}"); //check the error
        errback(new ApiResponseException(www.error));
    }
    else if (IsHttpError(www))
    {
        if (www.responseCode >= 500)
        {
            // TODO think of best way to map HTTP code to GRPC code since we can't rely
            // on server to process it. Manually adding the mapping to SDK seems brittle.
            errback(new ApiResponseException((int) www.responseCode, www.downloadHandler.text, -1));
            www.Dispose();
            yield break;
        }

        var decoded = www.downloadHandler.text.FromJson<Dictionary<string, object>>();

        var e = new ApiResponseException(www.downloadHandler.text);

        if (decoded != null)
        {
            var msg = decoded.ContainsKey("message") ? decoded["message"].ToString() : string.Empty;
            var grpcCode = decoded.ContainsKey("code") ? (int) decoded["code"] : -1;

            e = new ApiResponseException(www.responseCode, msg, grpcCode);

            if (decoded.ContainsKey("error"))
            {
                IHttpAdapterUtil.CopyResponseError(Instance, decoded["error"], e);
            }
        }

        errback(e);
    }
    else
    {
        callback?.Invoke(www.downloadHandler?.text);
    }

    www.Dispose();
}

and I could check browser console messages like this:

f386f3dcd5f6d63f3dc8c44f92bdecaf.js.gz:10 @@@@@@@@@@@@@@@ authenticate device start
UnityEngine.DebugLogHandler:Internal_Log(LogType, LogOption, String, Object)
UnityEngine.DebugLogHandler:LogFormat(LogType, Object, String, Object[])
UnityEngine.Logger:Log(LogType, Object)
UnityEngine.Debug:Log(Object)
Nakama.Snippets.<Awake>d__6:MoveNext()
System.Runtime.CompilerServices.AsyncVoidMethodBuilder:Start(<Awake>d__6&)
Nakama.Snippets.WebGLConnect:Awake()


f386f3dcd5f6d63f3dc8c44f92bdecaf.js.gz:10 @@@@@@@@@@@@@ Sending request: POST http://localhost:7350/v2/account/authenticate/device?create=true&
UnityEngine.DebugLogHandler:Internal_Log(LogType, LogOption, String, Object)
UnityEngine.DebugLogHandler:LogFormat(LogType, Object, String, Object[])
UnityEngine.Logger:Log(LogType, Object)
UnityEngine.Debug:Log(Object)
Nakama.<SendRequest>d__13:MoveNext()
UnityEngine.SetupCoroutine:InvokeMoveNext(IEnumerator, IntPtr)
UnityEngine.MonoBehaviour:StartCoroutineManaged2(IEnumerator)
UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
Nakama.UnityWebRequestAdapter:SendAsync(String, Uri, IDictionary`2, Byte[], Int32, Nullable`1)
Nakama.<AuthenticateDeviceAsync>d__13:MoveNext()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1:Start(<AuthenticateDeviceAsync>d__13&)
Nakama.ApiClient:AuthenticateDeviceAsync(String, String, ApiAccountDevice, Nullable`1, String, Nullable`1)
Nakama.<>c__DisplayClass46_0:<AuthenticateDeviceAsync>b__0()
Nakama.<InvokeWithRetry>d__2`1:MoveNext()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1:Start(<InvokeWithRetry>d__2`1&)
Nakama.RetryInvoker:InvokeWithRetry(Func`1, RetryHistory)
Nakama.<AuthenticateDeviceAsync>d__46:MoveNext()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1:Start(<AuthenticateDeviceAsync>d__46&)
Nakama.Client:AuthenticateDeviceAsync(String, String, Boolean, Dictionary`2, RetryConfiguration, CancellationToken)
Nakama.Snippets.<Awake>d__6:MoveNext()
System.Runtime.CompilerServices.AsyncVoidMethodBuilder:Start(<Awake>d__6&)
Nakama.Snippets.WebGLConnect:Awake()


WebGL_Test.loader.js:1 [UnityCache] 'http://localhost:59310/Build/ad4923f1c5efdd2594ec8d6bc490335d.data.gz' successfully downloaded and stored in the indexedDB cache
:7350/v2/account/authenticate/device?create=true&:1 
        
        
       Failed to load resource: net::ERR_CONNECTION_REFUSEDUnderstand this errorAI
f386f3dcd5f6d63f3dc8c44f92bdecaf.js.gz:10 @@@@@@@@@@@@@ Response: 500 
UnityEngine.DebugLogHandler:Internal_Log(LogType, LogOption, String, Object)
UnityEngine.DebugLogHandler:LogFormat(LogType, Object, String, Object[])
UnityEngine.Logger:Log(LogType, Object)
UnityEngine.Debug:Log(Object)
Nakama.<SendRequest>d__13:MoveNext()
UnityEngine.SetupCoroutine:InvokeMoveNext(IEnumerator, IntPtr)


f386f3dcd5f6d63f3dc8c44f92bdecaf.js.gz:10 @@@@@@@@@@@@@ Network error: Unknown Error
UnityEngine.DebugLogHandler:Internal_Log(LogType, LogOption, String, Object)
UnityEngine.DebugLogHandler:LogFormat(LogType, Object, String, Object[])
UnityEngine.Logger:Log(LogType, Object)
UnityEngine.Debug:Log(Object)
Nakama.<SendRequest>d__13:MoveNext()
UnityEngine.SetupCoroutine:InvokeMoveNext(IEnumerator, IntPtr)

Maybe I guess the error callback is executed, but it doesn’t seem to be passed. :sweat_smile: