question

esk avatar image
esk asked

Error when using local CloudScript on Azure Functions

When the playfab client API calls the locally running azure functions cloudscript from Unity, the response always gives an error

SerializationException: Invalid JSON string
PlayFab.Json.PlayFabSimpleJson.DeserializeObject (System.String json) (at Assets/PlayFabSDK/Shared/Internal/SimpleJson.cs:570)
PlayFab.Json.PlayFabSimpleJson.DeserializeObject (System.String json, System.Type type, PlayFab.Json.IJsonSerializerStrategy jsonSerializerStrategy) (at Assets/PlayFabSDK/Shared/Internal/SimpleJson.cs:602)
PlayFab.Json.PlayFabSimpleJson.DeserializeObject[T] (System.String json, PlayFab.Json.IJsonSerializerStrategy jsonSerializerStrategy) (at Assets/PlayFabSDK/Shared/Internal/SimpleJson.cs:610)
PlayFab.Json.SimpleJsonInstance.DeserializeObject[T] (System.String json) (at Assets/PlayFabSDK/Shared/Internal/ISerializer.cs:85)
PlayFab.Internal.PlayFabUnityHttp.OnResponse (System.String response, PlayFab.Internal.CallRequestContainer reqContainer) (at Assets/PlayFabSDK/Shared/Internal/PlayFabHttp/PlayFabUnityHttp.cs:176)
UnityEngine.Debug:LogException(Exception)
PlayFab.Internal.PlayFabUnityHttp:OnResponse(String, CallRequestContainer) (at 

I have even tried returning a simple string from the function to the client, but it gives the same error. I am using the latest ExecuteFunction.cs that is provided here https://github.com/PlayFab/pf-af-devfuncs/blob/main/csharp/ExecuteFunction.cs, and have made a Github issue here https://github.com/PlayFab/pf-af-devfuncs/issues/12

It works fine when calling the live function on azure cloud, it's just locally it doesn't work.

This is very important as I need to develop locally without having to affect production! The local workflow is already a bit terrible, I have to create and delete a json file just to get it to use the local functions URL, I can't even change it from code!!!

CloudScript
3 comments
10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Rick Chen avatar image Rick Chen ♦ commented ·

Could you please provide the relevant code snippet of your Azure function and your unity client for us to diagnose? Here is a similar thread that you could look into: https://community.playfab.com/questions/8487/serializationexception-invalid-json-string-in-unit.html.

0 Likes 0 ·
esk avatar image esk commented ·

It doesn't matter what I return from the azure function, a string, an empty object, an object with only alphanumeric keys and values, everything gives this error!

0 Likes 0 ·
Rick Chen avatar image Rick Chen ♦ commented ·

I have done a test using the code and followed this document Tutorial: Local debugging for Cloudscript using Azure Functions to debug locally. But I cannot reproduce this error. This error seems to be reported from the Unity. Which version of the Unity, the PlayFab SDK and the PlayFab Unity Editor Extension you were using? Could you provide the relevant Unity code snippet for us to diagnose?

0 Likes 0 ·
JayZuo avatar image
JayZuo answered

The issue here might to be that while local debugging, Unity sets Accept-Encoding to "deflate, gzip" (This seems to be a black box, as in Rick's test, the Accept-Encoding is always set to "identity", so he cannot reproduce this issue and according to Unity doc, it's not recommended to set accept-encoding header and we should leave it for automatic handling).

When the request's Accept-Encoding accepts "gzip", ExecuteFunction will return compressed response body. However, it doesn't set corresponding Content-Encoding header, so Unity doesn't decompress the response body for you, which caused it to be an unreadable sting that can't be deserialized. And eventually, Unity SDK throws an "Invalid JSON string" error.

To fix this, we can modify CompressResponseBody method and add Content-Encoding header like:

// If client accepts gzip, compress
if (encodings.Contains("gzip", StringComparer.OrdinalIgnoreCase))
{
    using (var stream = new MemoryStream())
    {
        using (var gZipStream = new GZipStream(stream, CompressionLevel.Fastest, false))
        {
            gZipStream.Write(responseBytes, 0, responseBytes.Length);
        }
        responseBytes = stream.ToArray();
    }
    var content = new ByteArrayContent(responseBytes);
    content.Headers.ContentEncoding.Add("gzip");
    return content;
}

I've also submitted a PR https://github.com/PlayFab/pf-af-devfuncs/pull/13. Hope it helps.

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

esk avatar image
esk answered

Unity 2020.3.15f2, PlayFab 2.112.210816 SDK, basically latest everything.

The code snippet is essentially the most basic executefunction example given by the docs,

PlayFabCloudScriptAPI.ExecuteFunction(new ExecuteFunctionRequest()
        {
            Entity = new PlayFab.CloudScriptModels.EntityKey()
            {
                Id = PlayFabSettings.staticPlayer.EntityId, //Get this from when you logged in,
                Type = PlayFabSettings.staticPlayer.EntityType, //Get this from when you logged in
            },
            FunctionName = Constants.CloudScriptFunctionNames.PublishLevel, //This should be the name of your Azure Function that you created.
            FunctionParameter = payload, //This is the data that you would want to pass into your function.
            GeneratePlayStreamEvent = false //Set this to true if you would like this call to show up in PlayStream
        }, (ExecuteFunctionResult result) =>
        {
            if (result.FunctionResultTooLarge ?? false)
            {
                Debug.Log("This can happen if you exceed the limit that can be returned from an Azure Function, See PlayFab Limits Page for details.");
                t.TrySetException(new System.Exception("Function result too large"));
                return;
            }
            Debug.Log($"The {result.FunctionName} function took {result.ExecutionTimeMilliseconds} to complete");
            Debug.Log($"Result: {result.FunctionResult.ToString()}");


        }, (PlayFabError error) =>
        {
            Debug.Log($"PlayFab Error: {error.GenerateErrorReport()}");
        });

4 comments
10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

esk avatar image esk commented ·
0 Likes 0 ·
Rick Chen avatar image Rick Chen ♦ commented ·

I have used the code snippet you provide and done a test. I got the result successfully and I still cannot reproduce the issue.

y unity version is 2020.3.8f1, my PlayFabSDK version is 2.98.201027 and my PlayFab Editor Extensions version is 2.100.201207.

Have you used the PlayFab Editor Extensions in your project? If so, what is the version of it?

I will try on the version you mentioned. In the mean time, as it is hard to tell which part was causing the issue. Please follow the Debugging C# code in Unity to find out the specific part that was causing the issue.

0 Likes 0 ·
local-af-debug.png (62.4 KiB)
esk avatar image esk Rick Chen ♦ commented ·

Im using editor extensions version 2.110.210628. It may be worth pointing out that i'm on MacOS (Big Sur, intel), and using the Azure VSCode extension.

What are you returning from the azure function? I tried returning a class, and a string, neither of which work locally (but do when hosted)

0 Likes 0 ·
esk avatar image esk Rick Chen ♦ commented ·

Could you also share the executefunction.cs that you are using? maybe that's the issue.

0 Likes 0 ·
Rick Chen avatar image
Rick Chen answered

Sorry for the late reply. Currently it is hard for us to do the test. My Azure function was returning the result (object) of GetEntityProfile API call. Here is the codesnippet of my local Azure Function:

        [FunctionName("ExecuteFunction")]
        public static async Task<HttpResponseMessage> ExecuteFunction(
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = "CloudScript/ExecuteFunction")] HttpRequest request, ILogger log)
        {
            // Extract the caller's entity token
            string callerEntityToken = request.Headers["X-EntityToken"];
            // Extract the request body and deserialize
            string body = await DecompressHttpBody(request);
            var execRequest = PlayFabSimpleJson.DeserializeObject<ExecuteFunctionRequest>(body);
            EntityKey entityKey = null;
            if (execRequest.Entity != null)
            {
                entityKey = new EntityKey
                {
                    Id = execRequest.Entity?.Id,
                    Type = execRequest.Entity?.Type
                };
            }
            // Create a FunctionContextInternal as the payload to send to the target function
            var functionContext = new FunctionContextInternal
            {
                CallerEntityProfile = await GetEntityProfile(callerEntityToken, entityKey),
                TitleAuthenticationContext = new TitleAuthenticationContext
                {
                    Id = Environment.GetEnvironmentVariable(TITLE_ID, EnvironmentVariableTarget.Process),
                    EntityToken = await GetTitleEntityToken()
                },
                FunctionArgument = execRequest.FunctionParameter
            };
            // Serialize the request to the azure function and add headers
            var functionRequestContent = new StringContent(PlayFabSimpleJson.SerializeObject(functionContext));
            functionRequestContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
            functionRequestContent.Headers.Add("X-EntityToken",callerEntityToken);
            var azureFunctionUri = "https://[your title id].playfabapi.com/Profile/GetProfile";
            var sw = new Stopwatch();
            sw.Start();
            // Execute the local azure function
            using (var functionResponseMessage =
                await httpClient.PostAsync(azureFunctionUri, functionRequestContent))
            {
                sw.Stop();
                long executionTime = sw.ElapsedMilliseconds;
                if (!functionResponseMessage.IsSuccessStatusCode)
                {
                    throw new Exception($"An error occured while executing the target function locally: FunctionName: {execRequest.FunctionName}, HTTP Status Code: {functionResponseMessage.StatusCode}.");
                }
                // Extract the response content
                using (var functionResponseContent = functionResponseMessage.Content)
                {
                    // Prepare a response to reply back to client with and include function execution results
                    var functionResult = new ExecuteFunctionResult
                    {
                        FunctionName = execRequest.FunctionName,
                        FunctionResult = await ExtractFunctionResult(functionResponseContent),
                        ExecutionTimeMilliseconds = (int)executionTime,
                        FunctionResultTooLarge = false
                    };
                    // Reply back to client with final results
                    var output = new PlayFabJsonSuccess<ExecuteFunctionResult>
                    {
                        code = 200,
                        status = "OK",
                        data = functionResult
                    };
                    // Serialize the output and return it
                    var outputStr = PlayFabSimpleJson.SerializeObject(output);
                    return new HttpResponseMessage
                    {
                        Content = new ByteArrayContent(CompressResponseBody(output, request)),
                        StatusCode = HttpStatusCode.OK
                    };
                }
            }
        }

Here is my code snippet of how the local function is called in Unity:

PlayFabCloudScriptAPI.ExecuteFunction(new ExecuteFunctionRequest()
        {
            Entity = new PlayFab.CloudScriptModels.EntityKey()
            {
                Id = PlayFabSettings.staticPlayer.EntityId, //Get this from when you logged in,
                Type = PlayFabSettings.staticPlayer.EntityType, //Get this from when you logged in
            },
            FunctionName = "Constants.CloudScriptFunctionNames.PublishLevel", //This should be the name of your Azure Function that you created.
            FunctionParameter = "payload", //This is the data that you would want to pass into your function.
            GeneratePlayStreamEvent = false //Set this to true if you would like this call to show up in PlayStream
        }, (ExecuteFunctionResult result) =>
        {
            if (result.FunctionResultTooLarge ?? false)
            {
                Debug.Log("This can happen if you exceed the limit that can be returned from an Azure Function, See PlayFab Limits Page for details.");
                //t.TrySetException(new System.Exception("Function result too large"));
                return;
            }
            Debug.Log($"The {result.FunctionName} function took {result.ExecutionTimeMilliseconds} to complete");
            Debug.Log($"Result: {result.FunctionResult.ToString()}");
        }, (PlayFabError error) =>
        {
            Debug.Log($"PlayFab Error: {error.GenerateErrorReport()}");
        });

As the resources for us is limited, it could take a while for us to set up the test. Your patience is appreciated.

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

esk avatar image
esk answered

The solution I finally figured out is to set "Accept-Encoding" to "identity" in the extraHeaders argument when calling ExecuteFunction. There seems to be some problem with the gzip encoding, hopefully the PlayFab team will figure it out as this seems to be an issue quite a few people are having. I was personally having this issue when developing locally, but now I seem to have it when using the cloud hosted Azure Functions directly too, and I don't know why!

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Michael Urvan avatar image
Michael Urvan answered

wow thank you for saving me so much time!

Since the compression only breaks the local testing - to fix this (so you don't have to make all your client calls pass the Accept-Encoding which might be breaking the normal cloud azure calls too), just modify your LocalExecuteFunction() code that they told us to add for local testing:

for DecompressHttpBody()

on the 3rd line down change it to:

if (true) //if (string.IsNullOrWhiteSpace(encoding))

for CompressResponseBody()

on the 8th line down change it to:

if (true)//if (string.IsNullOrEmpty(encodingsString))

1 comment
10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Tô Chí Thành avatar image Tô Chí Thành commented ·

Thank you, your solution is worked for me.

0 Likes 0 ·

Write an Answer

Hint: Notify or tag a user in this post by typing @username.

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.