// Callback for "Get Ticket" UnityWebRequestAsyncOperation completion
        void OnGetTicketAsyncCompleted(AsyncOperation obj)
        {
            if (!(obj is UnityWebRequestAsyncOperation getTicketOp))
            {
                throw new ArgumentException(logPre + "Wrong AsyncOperation type in callback.");
            }

            Protobuf.GetTicketResponse getResponse;

            // Every return statement in here will trigger finally{} cleanup
            try
            {
                // Short-circuit if we're in a terminal state or have no Get call registered
                if (IsDone || m_GetTicketAsyncOperation == null)
                {
                    return;
                }

                if (getTicketOp != m_GetTicketAsyncOperation)
                {
                    throw new InvalidOperationException(logPre + $"Wrong operation object received by {nameof(OnGetTicketAsyncCompleted)}.");
                }

                // No need to log on an expected abort state
                if (State == MatchmakingRequestState.Canceling || MatchmakingClient.IsWebRequestAborted(getTicketOp.webRequest))
                {
                    return;
                }

                // This helps to ensure that GET calls aborted during a delete are not processed
                if (State != MatchmakingRequestState.Polling)
                {
                    Debug.LogWarning(logPre + "Ignoring Matchmaking Get Ticket response while not in polling state.");
                    return;
                }

                if (MatchmakingClient.IsWebRequestFailed(getTicketOp.webRequest))
                {
                    HandleError($"Error getting matchmaking ticket: {getTicketOp.webRequest.error}");
                    return;
                }

                // Parse the body of the response - only try if we actually have a body
                // Successful responses w/o bodies are not considered failures
                if (getTicketOp.webRequest?.downloadHandler?.data?.Length == 0)
                {
                    return;
                }

                // Try to parse body
                if (!MatchmakingClient.TryParseGetTicketResponse(getTicketOp.webRequest, out getResponse))
                {
                    // Body was present but we couldn't parse it; this is probably a fatal error
                    HandleError($"Error parsing GET response for ticket");
                    return;
                }
            }
            catch (Exception e)
            {
                HandleError($"Error getting information for ticket: {e.Message}");
                return;
            }
            finally
            {
                // Allow the operation to get garbage collected
                if (getTicketOp == m_GetTicketAsyncOperation)
                {
                    m_GetTicketAsyncOperation = null;
                    m_LastPollTime            = Time.unscaledTime;
                }

                getTicketOp.webRequest?.Dispose();
            }

            // Consume the response

            if (getResponse.Status != null)
            {
                HandleError($"Error getting matchmaking ticket. Code {getResponse.Status.Code}: {getResponse.Status.Message}");
                return;
            }

            // Check to see if this GET call has an assignment
            //  If assignment is null, ticket hasn't completed matchmaking yet
            if (getResponse.Assignment != null)
            {
                var errorExists      = !string.IsNullOrEmpty(getResponse.Assignment.Error);
                var connectionExists = !string.IsNullOrEmpty(getResponse.Assignment.Connection);
                var propertiesExists = !string.IsNullOrEmpty(getResponse.Assignment.Properties);

                // Note that assignment can have null fields
                Assignment = new Assignment(getResponse.Assignment.Connection, getResponse.Assignment.Error, getResponse.Assignment.Properties);

                // Set to ErrorRequest state if assignment has no real data
                if (!errorExists && !connectionExists && !propertiesExists)
                {
                    HandleError("Error getting matchmaking ticket: assignment returned by service could not be processed");
                    return;
                }

                // Set to ErrorAssignment state if parsed assignment object contains an error entry
                if (errorExists)
                {
                    State = MatchmakingRequestState.ErrorAssignment;
                    Debug.LogError(logPre + $"Matchmaking completed with Assignment error: {Assignment.Error}");
                    SetTerminalState();
                    return;
                }

                // No error and valid connection and/or properties - set to AssignmentReceived
                State = MatchmakingRequestState.AssignmentReceived;
                Debug.Log(logPre + $"Matchmaking completed successfully; connection information received.");
                SetTerminalState();
            }
        }
        // Callback for "Delete Ticket" UnityWebRequestAsyncOperation completion
        void OnDeleteTicketAsyncCompleted(AsyncOperation obj)
        {
            if (!(obj is UnityWebRequestAsyncOperation deleteTicketOp))
            {
                throw new ArgumentException(logPre + "Wrong AsyncOperation type in callback.");
            }

            // Every return statement in here will trigger finally{} cleanup
            try
            {
                // Short-circuit if we're in a terminal state or have no deletion registered
                if (IsDone || m_DeleteTicketAsyncOperation == null)
                {
                    return;
                }

                if (deleteTicketOp != m_DeleteTicketAsyncOperation)
                {
                    throw new InvalidOperationException(logPre + $"Wrong operation object received by {nameof(OnDeleteTicketAsyncCompleted)}.");
                }

                // If the request here was a success, we have successfully deleted a ticket
                if (MatchmakingClient.IsWebRequestFailed(deleteTicketOp.webRequest))
                {
                    Debug.LogError(logPre + $"Error deleting matchmaking ticket: {deleteTicketOp.webRequest.error}");
                    return;
                }

                // Everything after this is just to detect if there was additional information
                //  provided in the body of the DELETE response

                // If no body, we're done
                if (deleteTicketOp.webRequest?.downloadHandler?.data?.Length == 0)
                {
                    return;
                }

                // Try to parse body
                if (!MatchmakingClient.TryParseDeleteTicketResponse(deleteTicketOp.webRequest, out var result))
                {
                    // Delete succeeded but additional information could not be parsed; not a fatal error
                    Debug.LogError(logPre + $"Error parsing DELETE information for ticket");
                    return;
                }

                // Handle the status code returned inside the DELETE body
                if (result?.Status != null && result.Status.Code != 0)
                {
                    Debug.LogError(logPre + $"Error deleting matchmaking ticket. Code {result.Status.Code}: {result.Status.Message}");
                    return;
                }

                // Success!
                Debug.Log(logPre + $"Matchmaking ticket deleted successfully");
            }
            catch (Exception e)
            {
                Debug.LogError(logPre + $"Error deleting matchmaking ticket: {e.Message}");
            }
            finally
            {
                // Allow the operation to get garbage collected
                if (deleteTicketOp == m_DeleteTicketAsyncOperation)
                {
                    m_DeleteTicketAsyncOperation = null;
                }

                deleteTicketOp.webRequest?.Dispose();

                // Handle failed deletion cases as a successful cancel
                HandleDelete();
            }
        }
        // Callback for "Create Ticket" UnityWebRequestAsyncOperation completion
        void OnCreateTicketAsyncCompleted(AsyncOperation obj)
        {
            if (!(obj is UnityWebRequestAsyncOperation createTicketOp))
            {
                throw new ArgumentException(logPre + "Wrong AsyncOperation type in callback.");
            }

            Protobuf.CreateTicketResponse result;

            // Every return statement in here will trigger finally{} cleanup
            try
            {
                // Short-circuit if we're in a terminal state or have no creation registered
                if (IsDone || m_CreateTicketAsyncOperation == null)
                {
                    return;
                }

                if (createTicketOp != m_CreateTicketAsyncOperation)
                {
                    throw new InvalidOperationException(logPre + $"Wrong operation object received by {nameof(OnCreateTicketAsyncCompleted)}.");
                }

                if (State != MatchmakingRequestState.Creating && State != MatchmakingRequestState.Canceling)
                {
                    Debug.LogWarning(logPre + "Ignoring Matchmaking Create Ticket response while not in creating state.");
                    return;
                }

                if (MatchmakingClient.IsWebRequestFailed(createTicketOp.webRequest))
                {
                    // If we tried to cancel while the Create call was being constructed or in flight,
                    //   the Create request will be set to an aborted state (counts as an "IsFailed" state)
                    if (State == MatchmakingRequestState.Canceling && MatchmakingClient.IsWebRequestAborted(createTicketOp.webRequest))
                    {
                        //if (MatchmakingClient.IsWebRequestSent(createTicketOp))
                        //Debug.LogWarning(logPre + "Matchmaking call was aborted, but ticket may have been created on the service end.");

                        Debug.Log(logPre + "Matchmaking was aborted during ticket creation");

                        HandleDelete();
                        return;
                    }

                    HandleError($"Error creating matchmaking ticket: {createTicketOp.webRequest.error}");
                    return;
                }

                // Parse the body of the response - only try if we actually have a body
                if (!MatchmakingClient.TryParseCreateTicketResponse(createTicketOp.webRequest, out result))
                {
                    // Could not parse the CREATE response; this is a fatal error
                    HandleError($"Error parsing CREATE response for ticket; could not get Ticket ID from service");
                }
            }
            catch (Exception e)
            {
                HandleError($"Error creating matchmaking ticket: {e.Message}");
                return;
            }
            finally
            {
                // Allow the operation to get garbage collected
                if (createTicketOp == m_CreateTicketAsyncOperation)
                {
                    m_CreateTicketAsyncOperation = null;
                }

                createTicketOp.webRequest?.Dispose();
            }

            // Try to consume the parsed response

            if (result == null)
            {
                HandleError("Error creating matchmaking ticket.");
                return;
            }

            if (result.Status != null)
            {
                HandleError($"Error creating matchmaking ticket. Code {result.Status.Code}: {result.Status.Message}");
                return;
            }

            if (string.IsNullOrEmpty(result.Id))
            {
                HandleError("Error creating matchmaking ticket. Id not set.");
                return;
            }

            // We were able to parse the Ticket Id
            TicketId = result.Id;

            // If a cancellation request is queued, send the cancellation instead of polling for assignment
            if (State == MatchmakingRequestState.Canceling)
            {
                DeleteMatchRequestTicket();
                return;
            }

            // Start polling for the assignment for the assigned Ticket Id
            Debug.Log(logPre + $"Ticket ID received; polling matchmaking for assignment");
            StartGetTicketRequest(TicketId);
        }