"WebRequestException: Request timeout" on Services.Authentication.AuthenticationServiceInternal

Hello everybody,
we managed to upgrade our Unity to the "latest" 2022 version (2022.1.17f1) and upgraded every unity-pack. Since we now have to Authenticate the users (for RemoteConfig/Ads/IAP) we implemented it, like the documentations mentioning.

Since the implementation, we have a tremendous rise of our exceptions because of the authentication-system (the second high one is the "System.Collections.Generic.Dictionary`2[TKey,TValue].TryInsert"-
TelemetryHandler one, that's another story).

I post this screenshot attached, as I start thinking, this might have something to do with traffic? If you look at the hour-graph you may understand why i came to this conclusion.
8668953--1167642--upload_2022-12-17_11-26-12.png

I am not able to reproduce this exception and I don't know if this is a critical exception for IAP's or other game-relevant stuff.

I want to implement more of the UGS-features (without fearing it doesn't work like intended) and I just hope maybe someone can help me or deliver some suggestions, leading to a new perspective/solution-attempt.


Here's the stacktrace:

Unity.Services.Authentication.AuthenticationServiceInternal/<HandleSignInRequestAsync>d__108.MoveNext() (at D:/XXX/Library/PackageCache/com.unity.services.authentication@2.3.1/Runtime/AuthenticationServiceInternal.cs:528)
Unity.Services.Authentication.WebRequest/<SendAsync>d__15`1<System.Object>.MoveNext() (at D:/XXX/Library/PackageCache/com.unity.services.authentication@2.3.1/Runtime/Network/WebRequest.cs:63)
TMPro.TMP_TextProcessingStack`1<Unity.IL2CPP.Metadata.__Il2CppFullySharedGenericType>.PreviousItem() (at D:/XXX/Library/PackageCache/com.unity.textmeshpro@3.0.6/Scripts/Runtime/TMP_TextProcessingStack.cs:397)
TMPro.TMP_TextProcessingStack`1<Unity.IL2CPP.Metadata.__Il2CppFullySharedGenericType>.PreviousItem() (at D:/XXX/Library/PackageCache/com.unity.textmeshpro@3.0.6/Scripts/Runtime/TMP_TextProcessingStack.cs:397)
Unity.Services.Authentication.WebRequest.RequestCompleted(System.Threading.Tasks.TaskCompletionSource`1<System.String>,System.Int64,System.Boolean,System.Boolean,System.String,System.String,System.Collections.Generic.IDictionary`2<System.String,System.String>) (at D:/EXXX/Library/PackageCache/com.unity.services.authentication@2.3.1/Runtime/Network/WebRequest.cs:193)
Unity.Services.Authentication.WebRequest/<>c__DisplayClass16_0.<SendAttemptAsync>b__0(UnityEngine.AsyncOperation) (at D:/XXX/Library/PackageCache/com.unity.services.authentication@2.3.1/Runtime/Network/WebRequest.cs:82)

And here's my implementation-Code:

private bool isTryingToInit = false;
        private bool isInitialized = false;

        private UnityAction<bool> onConnectionOutcomeEvent;

        private const string environment = "production";

        private bool HasConnection {
            get {
                return (Application.internetReachability != NetworkReachability.NotReachable);
                //retrun Unity.Services.RemoteConfig.Utilities.CheckForInternetConnection(); //thats a "http"-call??? - no, thanks
            }
        }

        public void TryConnectWithUnityService(UnityAction<bool> _onConnecitonOutcome) {
            if (isInitialized) {
                if (_onConnecitonOutcome != null) {
                    _onConnecitonOutcome.Invoke(true);
                }
                return;
            }
            if (isTryingToInit) {
                if (_onConnecitonOutcome != null) {
                    onConnectionOutcomeEvent -= _onConnecitonOutcome;
                    onConnectionOutcomeEvent += _onConnecitonOutcome;
                }
                return;
            }
            isTryingToInit = true;

            if (!HasConnection) {

                isTryingToInit = false;
                if (_onConnecitonOutcome != null) {
                    _onConnecitonOutcome.Invoke(false);
                }
                return;
            }

            if (_onConnecitonOutcome != null) {
                onConnectionOutcomeEvent -= _onConnecitonOutcome;
                onConnectionOutcomeEvent += _onConnecitonOutcome;
            }
            StartConnectingUnityService();
        }

        private async void StartConnectingUnityService() {
            await InitializeAndSignInAsync();

            isInitialized = (UnityServices.State == ServicesInitializationState.Initialized);
            isTryingToInit = false;
            if(onConnectionOutcomeEvent != null) {
                UnityAction<bool> _tempHolder = onConnectionOutcomeEvent;
                onConnectionOutcomeEvent = null;
                _tempHolder.Invoke(isInitialized);
            }
        }

        private async Task InitializeAndSignInAsync() {
            // initialize handlers for unity game services
            if (!isInitialized) {
                if ((UnityServices.State == ServicesInitializationState.Uninitialized)) {
                    try {
                        // options can be passed in the initializer, e.g if you want to set analytics-user-id or an environment-name use the lines from below:
                        // var options = new InitializationOptions()
                        //   .SetOption("com.unity.services.core.analytics-user-id", "my-user-id-1234")
                        //   .SetOption("com.unity.services.core.environment-name", "production");
                        var _options = new InitializationOptions()
                        .SetEnvironmentName(environment);

                        await UnityServices.InitializeAsync(_options);
                    } catch (System.Exception _ex) {

                        return;
                    }
                }
            }

            // remote config requires authentication for managing environment information
            if (!AuthenticationService.Instance.IsSignedIn) {
                try {
                    await AuthenticationService.Instance.SignInAnonymouslyAsync();
                } catch (AuthenticationException ex) {
                    // Compare error code to AuthenticationErrorCodes
                    // Notify the player with the proper error message
                    Debug.LogException(ex);
                } catch (RequestFailedException ex) {
                    // Compare error code to CommonErrorCodes
                    // Notify the player with the proper error message
                    Debug.LogException(ex);
                } catch (System.Exception _ex) {

                    return;
                }
            }


        }

Relevant Packages we use:

  • Advertisement 4.3.0
  • Analytics 4.2.0
  • Analytics Library 3.8.1
  • Authentication 2.3.1
  • In App Purchasing 4.5.1
  • Remote Config 3.1.3

Other relevant Informations:

  • We use IL2CPP
  • We use proguard

Hello,

At first glance, I will say network exceptions are common, especially on mobile where the data connection can be unstable or slow.

Everytime you use Debug.LogException, the exception will be sent to Cloud Diagnostics.

For exceptions related to network errors (no network, timeout, etc.), I suggest avoiding logging these as exceptions.

(You can use Debug.LogError(exception.Message) in these cases if you want to keep the logs without the exception being sent to cloud diagnostics)

Could you provide your organization and project id so we can do a more in-depth analysis?

Thanks for the info about

Debug.LogException(exception)

I changed those 2 to

Debug.LogWarning(exception)

I'll PM you the organization and project id.

Hi @erickb_unity . An issue related to this is that**WebRequestException**is marked as an internal Exception-derived class to the**Unity.Services.Authentication**namespace. So it's not easy (without reflection) to catch this particular exception.

In other words, I prefer to do:

if (ex is WebRequestException)
// or
catch(WebRequestException ex)
// instead of
if (ex.GetType().FullName == "Unity.Services.Authentication.WebRequestException")

Hello, the WebRequestException should never be thrown from our apis. If you are catching it, can you let us know in what situation this is thrown?

WebRequestException are handled in the internal service logic and should always be converted to an AuthenticationException or RequestFailedException depending on the error.

Thanks @erickb_unity . That is strange then. It's showing up on our Unity Cloud Diagnostics. We logged the exception because it's caught in our code (unhandled by yours) after awaiting on
AuthenticationService.Instance.SignInAnonymouslyAsync(). This log should help you track it down...

Log Message Mar 20, 2023, 13:55:04.942 [UnityGameServices] Result of internet reach check: True
Log Message Mar 20, 2023, 13:55:06.606 [IAP_Store] Created static IAP store singleton
Log Message Mar 20, 2023, 13:55:06.607 [IAP_Store] Init Unity IAP v4.7.0 for GooglePlay with 10 products
Error Mar 20, 2023, 13:55:09.441 Curl error 60: Cert verify failed: UNITYTLS_X509VERIFY_FLAG_USER_ERROR1
Error Mar 20, 2023, 13:55:10.001 Curl error 28: SSL connection timeout
Warning Mar 20, 2023, 13:55:10.010 [Authentication]: Network error detected, retrying...
Error Mar 20, 2023, 13:55:15.012 Curl error 28: Operation timed out after 4999 milliseconds with 0 bytes received
Warning Mar 20, 2023, 13:55:15.038 [Authentication]: Network error detected, retrying...
Error Mar 20, 2023, 13:55:20.041 Curl error 28: Operation timed out after 4999 milliseconds with 0 bytes received
Log Message Mar 20, 2023, 13:55:20.067 [Authentication]: Request failed: 0, Request timeout

Notice first that our code (UnityGameServices) performs an HttpWebRequest to "https://unity.com" and that succeeds (first line). Meanwhile both the GooglePlayGames plugin (not shown, but it was previously in the log) and Unity IAP are able to connect and authenticate. Then Curl errors start appearing which I believe are from Unity Authentication Service. The last line with the timeout is then logged and then after, the exception is thrown with WebRequestException.Message = "Request timeout."

Package versions:

  • Unity Authentication v2.4.0
  • Services Core 1.8.1

thoughts @erickb_unity ?

Hello

Sorry for missing your previous message.

I think I now understand why this is happening.

On the Authentication SDK side, we only send AuthenticationException or RequestFailedException with the WebRequestException as the internal exception provided within. Only these two can be caught.

If the exception is not caught by you, the unity engine will catch it and will unwrap the internal exception(s), resulting in "WebRequestException: Request timeout".

This is also what cloud diagnostics operates on so it would show the same.

I will have to discuss if it's best if we don't include the internal exception into our Authentication & RequestFailedException in this case. The WebRequestException should only be internal logic that you should not need to worry about.

I would also recommend you always try-catch network operations like this (Using either AuthenticationException+RequestFailedException or a more generic Exception) and avoid using LogException for these.

[quote=“erickb_unity”, post:8, topic: 903506]
If the exception is not caught by you, the unity engine will catch it and will unwrap the internal exception(s), resulting in “WebRequestException: Request timeout”.
[/quote]
Well except we ARE catching the exception. We only sent it to Debug.LogException when it’s one that we don’t recognize. In this case we were handling RequestFailedException but the exception we’re talking about has type of WebRequestException so since we didn’t recognize the type, we call Debug.LogException() on it.

This is the code we had before:

catch(System.Exception ex)
{
    if (ex is System.OperationCanceledException)
        return;
    if (ex is RequestFailedException)
        Debug.LogWarning($"[UnityGameServices] Unable to authenticate: {ex.Message}!");
    else
        Debug.LogException(ex);
}

Here’s the code now (which correctly avoids logging WebRequestException):

catch(System.Exception ex)
{
    if (ex is System.OperationCanceledException)
        return;
    string exName = ex.GetType().Name;
    if (ex is RequestFailedException || exName == "WebRequestException")
        Debug.LogWarning($"[UnityGameServices] Unable to authenticate: {exName}: {ex.Message}!");
    else
        Debug.LogException(ex);
}

And it sounds like you’re saying if we change it to this then it should work also:

catch(System.Exception ex)
{
    if (ex is System.OperationCanceledException)
        return;
    if (ex is RequestFailedException || ex is AuthenticationException)
    {
        string exName = ex.GetType().Name;
        Debug.LogWarning($"[UnityGameServices] Unable to authenticate: {exName}: {ex.Message}!");
    }
    else
        Debug.LogException(ex);
}

However, I have doubts about this because if ex is AuthenticationException then it’s GetType().Name should return “AuthenticationException” not “WebRequestException”

I've been trying to find a edge case where WebRequestException could pass through our service logic and I can't find any for that version (2.4.0).

I've simulated timeouts and I get a RequestFailedException.

Using Debug.LogException or not catching the exception will log "WebRequestException: Request timeout" due to the how the engine works, but the exception caught is still a RequestFailedException.

Looking at your code, it should really send "Debug.LogWarning($"[UnityGameServices] Unable to authenticate: {ex.Message}!");"...

Are these errors caught on device or from looking at cloud diagnostics?
If it's cloud diagnostics, are you sure you are only capturing the versions of your build with your latest changes?

If you have any more repro steps of information that would be relevant, I hope we can get to the bottom of this.

It's being logged in cloud diagnostics. So actually I went to look at the previous code in source control history and I was mistaken about the previous code that's in the production app. It's actually this:

catch(System.Exception ex)
{
    if (ex is System.OperationCanceledException)
        return;
    Debug.LogException(ex);
}

So you're right that it's probably just raising a RequestFailedException and then logging it to Cloud Diagnostics as a WebRequestException. So we'll just log the warning when it's type is RequestFailedException or AuthenticationException and then only LogException to diagnostics when it's not one of those known public types.