What's the validity of upload URLs obtained with "InitiateFileUploads"? According to the documentation (https://docs.microsoft.com/en-us/rest/api/playfab/data/file/initiate-file-uploads?view=playfab-rest), they are valid for 5 minutes, and in games we've been caching them for 4 minutes before obtaining a new one, to avoid doing the call every time and to upload the files as quickly as possible when apps suspend, and some months ago it worked with no issue.
However, at some point (not sure when) uploads have started failing ("SimplePutCall" returns success, but the uploads don't go through, and the call to "FinalizeFileUploads" indicates there's no pending operation to finalize). They seem to only succeed when the URL has not been used for an upload, regardless of how much time has passed. Has this changed at some point? What should we exactly expect regarding the validity of URLs?
Edit: code that reproduces the issue. Doing a file upload the first time, the console shows:
Starting the upload Finalizing the upload File upload success: meow
When doing the upload the second time, the console shows:
Starting the upload Finalizing the upload /File/FinalizeFileUploads: The file meow does not have an operation pending to finalize.
#if !DISABLE_PLAYFABENTITY_API && !DISABLE_PLAYFABCLIENT_API using PlayFab; using PlayFab.Internal; using System; using System.Collections.Generic; using System.Text; using UnityEngine; public class EntityFileExample : MonoBehaviour { public string entityId; // Id representing the logged in player public string entityType; // entityType representing the logged in player private readonly Dictionary<string, string> _entityFileJson = new Dictionary<string, string>(); private readonly Dictionary<string, string> _tempUpdates = new Dictionary<string, string>(); public string ActiveUploadFileName; public string NewFileName; public int GlobalFileLock = 0; // Kind of cheap and simple way to handle this kind of lock public string cachedURL = null; public string cachedURLFileName = null; void OnSharedFailure(PlayFabError error) { Debug.LogError(error.GenerateErrorReport()); GlobalFileLock -= 1; } void OnGUI() { if (!PlayFabClientAPI.IsClientLoggedIn() && GUI.Button(new Rect(0, 0, 100, 30), "Login")) Login(); if (PlayFabClientAPI.IsClientLoggedIn() && GUI.Button(new Rect(0, 0, 100, 30), "LogOut")) PlayFabClientAPI.ForgetAllCredentials(); if (PlayFabClientAPI.IsClientLoggedIn() && GUI.Button(new Rect(100, 0, 100, 30), "(re)Load Files")) LoadAllFiles(); if (PlayFabClientAPI.IsClientLoggedIn()) { // Display existing files _tempUpdates.Clear(); var index = 0; foreach (var each in _entityFileJson) { GUI.Label(new Rect(100 * index, 60, 100, 30), each.Key); var tempInput = _entityFileJson[each.Key]; var tempOutput = GUI.TextField(new Rect(100 * index, 90, 100, 30), tempInput); if (tempInput != tempOutput) _tempUpdates[each.Key] = tempOutput; if (GUI.Button(new Rect(100 * index, 120, 100, 30), "Save " + each.Key)) UploadFile(each.Key); index++; } // Apply any changes foreach (var each in _tempUpdates) _entityFileJson[each.Key] = each.Value; // Add a new file NewFileName = GUI.TextField(new Rect(100 * index, 60, 100, 30), NewFileName); if (GUI.Button(new Rect(100 * index, 90, 100, 60), "Create " + NewFileName)) UploadFile(NewFileName); } } void Login() { var request = new PlayFab.ClientModels.LoginWithCustomIDRequest { CustomId = SystemInfo.deviceUniqueIdentifier, CreateAccount = true }; PlayFabClientAPI.LoginWithCustomID(request, OnLogin, OnSharedFailure); } void OnLogin(PlayFab.ClientModels.LoginResult result) { entityId = result.EntityToken.Entity.Id; entityType = result.EntityToken.Entity.Type; } void LoadAllFiles() { if (GlobalFileLock != 0) throw new Exception("This example overly restricts file operations for safety. Careful consideration must be made when doing multiple file operations in parallel to avoid conflict."); GlobalFileLock += 1; // Start GetFiles var request = new PlayFab.DataModels.GetFilesRequest { Entity = new PlayFab.DataModels.EntityKey { Id = entityId, Type = entityType } }; PlayFabDataAPI.GetFiles(request, OnGetFileMeta, OnSharedFailure); } void OnGetFileMeta(PlayFab.DataModels.GetFilesResponse result) { Debug.Log("Loading " + result.Metadata.Count + " files"); _entityFileJson.Clear(); foreach (var eachFilePair in result.Metadata) { _entityFileJson.Add(eachFilePair.Key, null); GetActualFile(eachFilePair.Value); } GlobalFileLock -= 1; // Finish GetFiles } void GetActualFile(PlayFab.DataModels.GetFileMetadata fileData) { GlobalFileLock += 1; // Start Each SimpleGetCall PlayFabHttp.SimpleGetCall(fileData.DownloadUrl, result => { _entityFileJson[fileData.FileName] = Encoding.UTF8.GetString(result); GlobalFileLock -= 1; }, // Finish Each SimpleGetCall error => { Debug.Log(error); } ); } void UploadFile(string fileName) { if (GlobalFileLock != 0) throw new Exception("This example overly restricts file operations for safety. Careful consideration must be made when doing multiple file operations in parallel to avoid conflict."); ActiveUploadFileName = fileName; GlobalFileLock += 1; // Start InitiateFileUploads if (string.IsNullOrEmpty(cachedURL) || fileName != cachedURLFileName) { cachedURL = null; cachedURLFileName = null; var request = new PlayFab.DataModels.InitiateFileUploadsRequest { Entity = new PlayFab.DataModels.EntityKey { Id = entityId, Type = entityType }, FileNames = new List<string> { ActiveUploadFileName }, }; PlayFabDataAPI.InitiateFileUploads(request, (response) => { cachedURL = response.UploadDetails[0].UploadUrl; cachedURLFileName = fileName; OnInitFileUpload(cachedURL); }, OnInitFailed); } else { OnInitFileUpload(cachedURL); } } void OnInitFailed(PlayFabError error) { if (error.Error == PlayFabErrorCode.EntityFileOperationPending) { Debug.LogError("InitiateFileUploads failed: PlayFabErrorCode.EntityFileOperationPending"); // This is an error you should handle when calling InitiateFileUploads, but your resolution path may vary GlobalFileLock += 1; // Start AbortFileUploads var request = new PlayFab.DataModels.AbortFileUploadsRequest { Entity = new PlayFab.DataModels.EntityKey { Id = entityId, Type = entityType }, FileNames = new List<string> { ActiveUploadFileName }, }; PlayFabDataAPI.AbortFileUploads(request, (result) => { GlobalFileLock -= 1; UploadFile(ActiveUploadFileName); }, OnSharedFailure); GlobalFileLock -= 1; // Finish AbortFileUploads GlobalFileLock -= 1; // Failed InitiateFileUploads } else OnSharedFailure(error); } void OnInitFileUpload(string uploadURL) { Debug.Log("Starting the upload"); var now = DateTime.UtcNow; string payloadStr = now.ToLongDateString() + ", " + now.ToLongTimeString(); var payload = Encoding.UTF8.GetBytes(payloadStr); GlobalFileLock += 1; // Start SimplePutCall PlayFabHttp.SimplePutCall(uploadURL, payload, FinalizeUpload, error => { Debug.Log(error); } ); GlobalFileLock -= 1; // Finish InitiateFileUploads } void FinalizeUpload(byte[] _) { Debug.Log("Finalizing the upload"); GlobalFileLock += 1; // Start FinalizeFileUploads var request = new PlayFab.DataModels.FinalizeFileUploadsRequest { Entity = new PlayFab.DataModels.EntityKey { Id = entityId, Type = entityType }, FileNames = new List<string> { ActiveUploadFileName }, }; PlayFabDataAPI.FinalizeFileUploads(request, OnUploadSuccess, OnSharedFailure); GlobalFileLock -= 1; // Finish SimplePutCall } void OnUploadSuccess(PlayFab.DataModels.FinalizeFileUploadsResponse result) { Debug.Log("File upload success: " + ActiveUploadFileName); GlobalFileLock -= 1; // Finish FinalizeFileUploads } } #endif