using System; using System.Collections; using System.Threading; using System.Threading.Tasks; using Best.HTTP.Shared; using UnityEngine; #if WITH_UNITASK using Cysharp.Threading.Tasks; #endif namespace Best.HTTP { /// /// Represents an exception thrown during or as a result of a Task-based asynchronous HTTP operations. /// public sealed class AsyncHTTPException : Exception { /// /// Gets the status code of the server's response. /// public readonly int StatusCode; /// /// Gets the content sent by the server. This is usually an error page for 4xx or 5xx responses. /// public readonly string Content; public AsyncHTTPException(string message) : base(message) { } public AsyncHTTPException(string message, Exception innerException) : base(message, innerException) { } public AsyncHTTPException(int statusCode, string message, string content) :base(message) { this.StatusCode = statusCode; this.Content = content; } public override string ToString() { return string.Format("StatusCode: {0}, Message: {1}, Content: {2}, StackTrace: {3}", this.StatusCode, this.Message, this.Content, this.StackTrace); } } /// /// A collection of extension methods for working with HTTP requests asynchronously using . /// public static class HTTPRequestAsyncExtensions { /// /// Asynchronously sends an HTTP request and retrieves the response as an . /// /// The to send. /// A cancellation token that can be used to cancel the operation. /// /// A Task that represents the asynchronous operation. The Task will complete with the retrieved AssetBundle /// if the request succeeds. If the request fails or is canceled, the Task will complete with an exception. /// #if WITH_UNITASK public static UniTask GetAssetBundleAsync(this HTTPRequest request, CancellationToken token = default) #else public static Task GetAssetBundleAsync(this HTTPRequest request, CancellationToken token = default) #endif { return CreateTask(request, token, #if UNITY_2023_1_OR_NEWER async #endif (req, resp, tcs) => { switch (req.State) { // The request finished without any problem. case HTTPRequestStates.Finished: if (resp.IsSuccess) { var bundleLoadOp = AssetBundle.LoadFromMemoryAsync(resp.Data); #if !UNITY_2023_1_OR_NEWER HTTPUpdateDelegator.Instance.StartCoroutine(BundleLoader(bundleLoadOp, tcs)); #else await Awaitable.FromAsyncOperation(bundleLoadOp); tcs.TrySetResult(bundleLoadOp.assetBundle); #endif } else GenericResponseHandler(req, resp, tcs); break; default: GenericResponseHandler(req, resp, tcs); break; } }); } #if !UNITY_2023_1_OR_NEWER #if WITH_UNITASK static IEnumerator BundleLoader(AssetBundleCreateRequest req, UniTaskCompletionSource tcs) #else static IEnumerator BundleLoader(AssetBundleCreateRequest req, TaskCompletionSource tcs) #endif { yield return req; tcs.TrySetResult(req.assetBundle); } #endif /// /// Asynchronously sends an HTTP request and retrieves the raw . /// /// /// This method is particularly useful when you want to access the raw response without any specific processing /// like converting the data into a string, texture, or other formats. It provides flexibility in handling /// the response for custom or advanced use cases. /// /// The to send. /// An optional that can be used to cancel the operation. /// /// A that represents the asynchronous operation. The value of TResult is the raw . /// If the request completes successfully, the task will return the HTTPResponse. If there's an error during the request or if /// the request gets canceled, the task will throw an exception, which can be caught and processed by the calling method. /// /// Thrown if there's an error in the request or if the server returns an error status code. #if WITH_UNITASK public static UniTask GetHTTPResponseAsync(this HTTPRequest request, CancellationToken token = default) #else public static Task GetHTTPResponseAsync(this HTTPRequest request, CancellationToken token = default) #endif { return CreateTask(request, token, (req, resp, tcs) => { switch (req.State) { // The request finished without any problem. case HTTPRequestStates.Finished: tcs.TrySetResult(resp); break; default: GenericResponseHandler(req, resp, tcs); break; } }); } /// /// Asynchronously sends an and retrieves the response content as a string. /// /// The to send. /// A cancellation token that can be used to cancel the operation. /// /// A Task that represents the asynchronous operation. The Task will complete with the retrieved string content /// if the request succeeds. If the request fails or is canceled, the Task will complete with an exception. /// #if WITH_UNITASK public static UniTask GetAsStringAsync(this HTTPRequest request, CancellationToken token = default) #else public static Task GetAsStringAsync(this HTTPRequest request, CancellationToken token = default) #endif { return CreateTask(request, token, (req, resp, tcs) => { switch (req.State) { // The request finished without any problem. case HTTPRequestStates.Finished: if (resp.IsSuccess) tcs.TrySetResult(resp.DataAsText); else GenericResponseHandler(req, resp, tcs); break; default: GenericResponseHandler(req, resp, tcs); break; } }); } /// /// Asynchronously sends an and retrieves the response content as a . /// /// The to send. /// A cancellation token that can be used to cancel the operation. /// /// A Task that represents the asynchronous operation. The Task will complete with the retrieved /// if the request succeeds. If the request fails or is canceled, the Task will complete with an exception. /// #if WITH_UNITASK public static UniTask GetAsTexture2DAsync(this HTTPRequest request, CancellationToken token = default) #else public static Task GetAsTexture2DAsync(this HTTPRequest request, CancellationToken token = default) #endif { return CreateTask(request, token, (req, resp, tcs) => { switch (req.State) { // The request finished without any problem. case HTTPRequestStates.Finished: if (resp.IsSuccess) tcs.TrySetResult(resp.DataAsTexture2D); else GenericResponseHandler(req, resp, tcs); break; default: GenericResponseHandler(req, resp, tcs); break; } }); } /// /// Asynchronously sends an and retrieves the response content as a byte[]. /// /// The to send. /// A cancellation token that can be used to cancel the operation. /// /// A Task that represents the asynchronous operation. The Task will complete with the retrieved byte[] /// if the request succeeds. If the request fails or is canceled, the Task will complete with an exception. /// #if WITH_UNITASK public static UniTask GetRawDataAsync(this HTTPRequest request, CancellationToken token = default) #else public static Task GetRawDataAsync(this HTTPRequest request, CancellationToken token = default) #endif { return CreateTask(request, token, (req, resp, tcs) => { switch (req.State) { // The request finished without any problem. case HTTPRequestStates.Finished: if (resp.IsSuccess) tcs.TrySetResult(resp.Data); else GenericResponseHandler(req, resp, tcs); break; default: GenericResponseHandler(req, resp, tcs); break; } }); } /// /// Asynchronously sends an and deserializes the response content into an object of type T using JSON deserialization. /// /// The type to deserialize the JSON content into. /// The to send. /// A cancellation token that can be used to cancel the operation. /// /// A Task that represents the asynchronous operation. The Task will complete with the deserialized object /// if the request succeeds and the response content can be deserialized. If the request fails, is canceled, or /// the response cannot be deserialized, the Task will complete with an exception. /// #if WITH_UNITASK public static UniTask GetFromJsonResultAsync(this HTTPRequest request, CancellationToken token = default) #else public static Task GetFromJsonResultAsync(this HTTPRequest request, CancellationToken token = default) #endif { return HTTPRequestAsyncExtensions.CreateTask(request, token, (req, resp, tcs) => { switch (req.State) { // The request finished without any problem. case HTTPRequestStates.Finished: if (resp.IsSuccess) { try { tcs.TrySetResult(Best.HTTP.JSON.LitJson.JsonMapper.ToObject(resp.DataAsText)); } catch (Exception ex) { tcs.TrySetException(ex); } } else GenericResponseHandler(req, resp, tcs); break; default: GenericResponseHandler(req, resp, tcs); break; } }); } [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] #if WITH_UNITASK public static UniTask CreateTask(HTTPRequest request, CancellationToken token, Action> callback) #else public static Task CreateTask(HTTPRequest request, CancellationToken token, Action> callback) #endif { HTTPManager.Setup(); #if WITH_UNITASK var tcs = new UniTaskCompletionSource(); #else var tcs = new TaskCompletionSource(); #endif request.Callback = (req, resp) => { if (token.IsCancellationRequested) tcs.TrySetCanceled(); else callback(req, resp, tcs); }; if (token.CanBeCanceled) token.Register((state) => (state as HTTPRequest)?.Abort(), request); if (request.State == HTTPRequestStates.Initial) request.Send(); return tcs.Task; } #if WITH_UNITASK public static void GenericResponseHandler(HTTPRequest req, HTTPResponse resp, UniTaskCompletionSource tcs) #else public static void GenericResponseHandler(HTTPRequest req, HTTPResponse resp, TaskCompletionSource tcs) #endif { switch (req.State) { // The request finished without any problem. case HTTPRequestStates.Finished: if (!resp.IsSuccess) tcs.TrySetException(CreateException($"Request finished Successfully, but the server sent an error ({resp.StatusCode} - '{resp.Message}').", resp)); break; // The request finished with an unexpected error. The request's Exception property may contain more info about the error. case HTTPRequestStates.Error: Log(req, $"Request Finished with Error! {req.Exception?.Message} - {req.Exception?.StackTrace}"); tcs.TrySetException(CreateException("No Exception", null, req.Exception)); break; // The request aborted, initiated by the user. case HTTPRequestStates.Aborted: Log(req, "Request Aborted!"); tcs.TrySetCanceled(); break; // Connecting to the server is timed out. case HTTPRequestStates.ConnectionTimedOut: Log(req, "Connection Timed Out!"); tcs.TrySetException(CreateException("Connection Timed Out!")); break; // The request didn't finished in the given time. case HTTPRequestStates.TimedOut: Log(req, "Processing the request Timed Out!"); tcs.TrySetException(CreateException("Processing the request Timed Out!")); break; } } [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public static void Log(HTTPRequest request, string str) { HTTPManager.Logger.Verbose(nameof(HTTPRequestAsyncExtensions), str, request.Context); } [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public static Exception CreateException(string errorMessage, HTTPResponse resp = null, Exception ex = null) { if (resp != null) return new AsyncHTTPException(resp.StatusCode, resp.Message, resp.DataAsText); else if (ex != null) return new AsyncHTTPException(ex.Message, ex); else return new AsyncHTTPException(errorMessage); } } }