Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for new informative error messages #157

Merged
merged 2 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 86 additions & 62 deletions Runtime/Client/LootLockerBaseServerAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ public void SwitchURL(LootLocker.LootLockerEnums.LootLockerCallerRole mainCaller
callerRole = mainCallerRole;
}

private bool WebRequestSucceeded(UnityWebRequest webRequest)
{
return !
#if UNITY_2020_1_OR_NEWER
(webRequest.result == UnityWebRequest.Result.ProtocolError || webRequest.result == UnityWebRequest.Result.ConnectionError || !string.IsNullOrEmpty(webRequest.error));
#else
(webRequest.isHttpError || webRequest.isNetworkError || !string.IsNullOrEmpty(webRequest.error));
#endif
}

public void SendRequest(LootLockerServerRequest request, System.Action<LootLockerResponse> OnServerResponse = null)
{
StartCoroutine(coroutine());
Expand Down Expand Up @@ -97,7 +107,7 @@ IEnumerator coroutine()
if (!webRequest.isDone && timedOut)
{
LootLockerLogger.GetForLogLevel(LootLockerLogger.LogLevel.Warning)("Exceeded maxTimeOut waiting for a response from " + request.httpMethod + " " + url);
OnServerResponse?.Invoke(new LootLockerResponse() { hasError = true, statusCode = 408, text = "{\"error\": \"" + request.endpoint + " Timed out.\"}", Error = request.endpoint + " Timed out." });
OnServerResponse?.Invoke(LootLockerResponseFactory.Error<LootLockerResponse>(request.endpoint + " Timed out.", 408));
yield break;
}

Expand All @@ -112,55 +122,17 @@ IEnumerator coroutine()
LootLockerLogger.GetForLogLevel(LootLockerLogger.LogLevel.Error)(ObfuscateJsonStringForLogging(webRequest.downloadHandler.text));
}

LootLockerResponse response = new LootLockerResponse();
response.statusCode = (int)webRequest.responseCode;
#if UNITY_2020_1_OR_NEWER
if (webRequest.result == UnityWebRequest.Result.ProtocolError || webRequest.result == UnityWebRequest.Result.ConnectionError || !string.IsNullOrEmpty(webRequest.error))
#else
if (webRequest.isHttpError || webRequest.isNetworkError || !string.IsNullOrEmpty(webRequest.error))
#endif

if(WebRequestSucceeded(webRequest))
{
switch (webRequest.responseCode)
{
case 200:
response.Error = "";
break;
case 400:
response.Error = "Bad Request -- Your request has an error";
break;
case 402:
response.Error = "Payment Required -- Payment failed. Insufficient funds, etc.";
break;
case 401:
response.Error = "Unauthorized -- Your session_token is invalid";
break;
case 403:
response.Error = "Forbidden -- You do not have access";
break;
case 404:
response.Error = "Not Found";
break;
case 405:
response.Error = "Method Not Allowed";
break;
case 406:
response.Error = "Not Acceptable -- Purchasing is disabled";
break;
case 409:
response.Error = "Conflict -- Your state is most likely not aligned with the servers.";
break;
case 429:
response.Error = "Too Many Requests -- You're being limited for sending too many requests too quickly.";
break;
case 500:
response.Error = "Internal Server Error -- We had a problem with our server. Try again later.";
break;
case 503:
response.Error = "Service Unavailable -- We're either offline for maintenance, or an error that should be solvable by calling again later was triggered.";
break;
}

LootLockerResponse response = new LootLockerResponse();
response.statusCode = (int)webRequest.responseCode;
response.success = true;
response.text = webRequest.downloadHandler.text;
response.errorData = null;
OnServerResponse?.Invoke(response);
}
else
{
if ((webRequest.responseCode == 401 || webRequest.responseCode == 403) && LootLockerConfig.current.allowTokenRefresh && CurrentPlatform.Get() != Platforms.Steam && tries < maxRetry)
{
tries++;
Expand All @@ -170,23 +142,75 @@ IEnumerator coroutine()
else
{
tries = 0;
response.Error += " " + ObfuscateJsonStringForLogging(webRequest.downloadHandler.text);
LootLockerResponse response = new LootLockerResponse();
response.statusCode = (int)webRequest.responseCode;
response.success = false;
response.hasError = true;
response.text = webRequest.downloadHandler.text;
LootLockerLogger.GetForLogLevel(LootLockerLogger.LogLevel.Error)(response.Error);

LootLockerErrorData errorData = LootLockerJson.DeserializeObject<LootLockerErrorData>(webRequest.downloadHandler.text);
// Check if the error uses the "new" error style (https://docs.lootlocker.com/reference/error-codes)
if (errorData != null && !string.IsNullOrEmpty(errorData.code))
{
response.errorData = errorData;
}
else
{
errorData = new LootLockerErrorData();
switch (webRequest.responseCode)
{
case 400:
errorData.message = "Bad Request -- Your request has an error.";
errorData.code = "bad_request";
break;
case 401:
errorData.message = "Unauthorized -- Your session_token is invalid.";
errorData.code = "unauthorized";
break;
case 402:
errorData.message = "Payment Required -- Payment failed. Insufficient funds, etc.";
errorData.code = "payment_required";
break;
case 403:
errorData.message = "Forbidden -- You do not have access to this resource.";
errorData.code = "forbidden";
break;
case 404:
errorData.message = "Not Found -- The requested resource could not be found.";
errorData.code = "not_found";
break;
case 405:
errorData.message = "Method Not Allowed -- The selected http method is invalid for this resource.";
errorData.code = "method_not_allowed";
break;
case 406:
errorData.message = "Not Acceptable -- Purchasing is disabled.";
errorData.code = "not_acceptable";
break;
case 409:
errorData.message = "Conflict -- Your state is most likely not aligned with the servers.";
errorData.code = "conflict";
break;
case 429:
errorData.message = "Too Many Requests -- You're being limited for sending too many requests too quickly.";
errorData.code = "too_many_requests";
break;
case 500:
errorData.message = "Internal Server Error -- We had a problem with our server. Try again later.";
errorData.code = "internal_server_error";
break;
case 503:
errorData.message = "Service Unavailable -- We're either offline for maintenance, or an error that should be solvable by calling again later was triggered.";
errorData.code = "service_unavailable";
break;
default:
errorData.message = "Unknown error.";
break;
}
errorData.message += " " + ObfuscateJsonStringForLogging(webRequest.downloadHandler.text);
}
LootLockerLogger.GetForLogLevel(LootLockerLogger.LogLevel.Error)(errorData.message + (!string.IsNullOrEmpty(errorData.doc_url) ? " -- " + errorData.doc_url : ""));
OnServerResponse?.Invoke(response);
}

}
else
{
response.success = true;
response.hasError = false;
response.statusCode = (int)webRequest.responseCode;
response.text = webRequest.downloadHandler.text;
OnServerResponse?.Invoke(response);
}

}
Expand Down Expand Up @@ -278,7 +302,7 @@ UnityWebRequest CreateWebRequest(string url, LootLockerServerRequest request)
// Set the content type - NO QUOTES around the boundary
string contentType = String.Concat("multipart/form-data; boundary=--", Encoding.UTF8.GetString(boundary));

// Make my request object and add the raw body. Set anything else you need here
// Make my request object and add the raw text. Set anything else you need here
webRequest = new UnityWebRequest();
webRequest.SetRequestHeader("Content-Type", "multipart/form-data; boundary=--");
webRequest.uri = new Uri(url);
Expand Down
72 changes: 56 additions & 16 deletions Runtime/Client/LootLockerServerRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,35 +90,75 @@ public enum LootLockerHTTPMethod
UPDATE_FILE = 9
}

public class LootLockerErrorData
{
/// <summary>
/// A descriptive code identifying the error.
/// </summary>
public string code { get; set; }

/// <summary>
/// A link to further documentation on the error.
/// </summary>
public string doc_url { get; set; }

/// <summary>
/// A unique identifier of the request to use in contact with support.
/// </summary>
public string request_id { get; set; }

/// <summary>
/// A unique identifier for tracing the request through LootLocker systems, use this in contact with support.
/// </summary>
public string trace_id { get; set; }

/// <summary>
/// If the request was not a success this property will hold any error messages
/// </summary>
public string message { get; set; }
}

/// <summary>
/// All ServerAPI.SendRequest responses will invoke the callback using an instance of this class for easier handling in client code.
/// </summary>
public class LootLockerResponse
{
/// <summary>
/// TRUE if http error OR server returns an error status
/// HTTP Status Code
/// </summary>
public bool hasError { get; set; }
public int statusCode { get; set; }

/// <summary>
/// HTTP Status Code
/// Whether this request was a success
/// </summary>
public int statusCode { get; set; }
public bool success { get; set; }

/// <summary>
/// Raw text response from the server
/// <para>If hasError = true, this will contain the error message.</para>
/// Raw text/http body from the server response
/// </summary>
public string text { get; set; }

public bool success { get; set; }
/// <summary>
/// If this request was not a success, this structure holds all the information needed to identify the problem
/// </summary>
public LootLockerErrorData errorData { get; set; }

/// <summary>
/// TRUE if http error OR server returns an error status
/// </summary>
[Obsolete("This property is deprecated, use success instead")]
public bool hasError { get { return !success; } }

public string Error { get; set; }
/// <summary>
/// If the request was not a success this property will hold any error messages
/// </summary>
[Obsolete("This property is deprecated, replaced by the errorData.message property")]
public string Error { get { return errorData?.message; } }

/// <summary>
/// A texture downloaded in the webrequest, if applicable, otherwise this will be null.
/// </summary>
[Obsolete("This property is deprecated")]
public Texture2D texture { get; set; }

/// <summary>
Expand Down Expand Up @@ -150,18 +190,18 @@ public static T Deserialize<T>(LootLockerResponse serverResponse,
{
if (serverResponse == null)
{
return new T() { success = false, Error = "Unknown error, please check your internet connection." };
return LootLockerResponseFactory.Error<T>("Unknown error, please check your internet connection.");
}
else if (!string.IsNullOrEmpty(serverResponse.Error))
else if (serverResponse.errorData != null && !string.IsNullOrEmpty(serverResponse.errorData.code))
{
return new T() { success = false, Error = serverResponse.Error, statusCode = serverResponse.statusCode };
return new T() { success = false, errorData = serverResponse.errorData, statusCode = serverResponse.statusCode };
}

var response = LootLockerJson.DeserializeObject<T>(serverResponse.text, options ?? LootLockerJsonSettings.Default) ?? new T();

response.text = serverResponse.text;
response.success = serverResponse.success;
response.Error = serverResponse.Error;
response.errorData = serverResponse.errorData;
response.statusCode = serverResponse.statusCode;

return response;
Expand All @@ -182,14 +222,14 @@ public class LootLockerResponseFactory
/// <summary>
/// Construct an error response to send to the client.
/// </summary>
public static T Error<T>(string errorMessage) where T : LootLockerResponse, new()
public static T Error<T>(string errorMessage, int statusCode = 0) where T : LootLockerResponse, new()
{
return new T()
{
success = false,
hasError = true,
Error = errorMessage,
text = errorMessage
text = errorMessage,
statusCode = statusCode,
errorData = new LootLockerErrorData() { message = errorMessage }
};
}

Expand Down
Loading
Loading