WebRequest
A robust HTTP client solution for Unity, offering flexible API management, automatic retries, and comprehensive request handling using .NET's HttpClient.
In One Line
REST assured, your requests are in good hands!
Overview
AceLand WebRequest is a modern web request solution designed specifically for Unity projects, providing a fluent interface for API calls with built-in error handling, retry mechanisms, and API section management. It uses .NET's HttpClient instead of UnityWebRequest for better performance and more control.
Package Info
display name
AceLand WebRequest
package name
com.aceland.webrequestlatest version
2.0.1
namespace
AceLand.WebRequestgit repository
dependencies
com.aceland.library: 2.0.3
com.aceland.taskutils: 2.0.0
com.unity.nuget.newtonsoft-json: 3.2.1Why Use It?
Modern Architecture: Built on .NET's HttpClient for better performance and reliability
Flexible API Management: Test and switch between different API configurations in editor
Built-in Retry Logic: Automatic retry mechanism with configurable intervals
Type-Safe Builder Pattern: Fluent interface for building requests with compile-time safety
Comprehensive Error Handling: Detailed error logging and exception management
Multiple Content Types: Support for JSON, Form, and Multipart request types
Key Features
Custom retry intervals and timeout settings
Comprehensive logging system with different build levels
HTTPS enforcement option
Automatic timestamp header injection
JSON validation before sending
Support for cancellation tokens
Editor tools for API section management
Project Settings
Logging
---
Logging Level
Level of Logging on web request
default: BuildLevel.Production
Result Logging Level
Level of Logging on request success
default: BuildLevel.DevelopmentBuild
Checking Options
---
Check Json Before Send
Check given content is a valid json before send request.
This may be expensive if big content.
Default: false
Force Https Scheme
Require request url is https if true. Otherwise http will also ok.
Default: true
Header Auto Fill
---
Add Time In Header
Add send time in header automatically.
Default: true
Time Key
key of Time in header.
default: Time
Auto Fill Headers
Add default headers on each Request.
Same headers will be covered by WithHeaders options on building Request.
Default: { "User-Agent", "Mozilla/5.0" }
Request Options
---
Request Timeout
Default timeout if not set in request handle.
Default: 3000 ms
Long Request Timeout
Default long request timeout. It will be set if building request handle with long request.
Default: 15000 ms
Request Retry
How many times will retry on connection error including timeout.
Default: 3
Retry Interval
Interval between each retry.
Default: [ 400, 800, 1600, 3200, 6400, 12800, 25600] in ms
Current API Section
---
Section
[Lock] Current API section.
Domain
[Lock] Current API Domain.
Version
[Lock] Current API Version.
- button
Clear Current API Section Data.
Api Url
[Lock] API URL of current section.
API Section Editor
---
Table
This is API Sections Data. All data is saved in
Assets/Editor/AceLand/api_sections.asset that confirms non-used data will not be built.
Last line is an empty line for adding new data.
- delete data * apply to Current API Section
+ add data
Save save to asset Restore restore unsaved changes
Usage
// Create a GET Request Handler
IRequestHandle request = Request.Get()
.WithUrl("https://api.example.com/data".ToUri())
.WithHeader("User-Agent", "MyUnityApp/1.0")
.Build();
// Create a POST Request Handler
IRequestHandle request = Request.Post()
.WithUrl("https://api.example.com/users".ToUri())
.WithJsonContent()
.WithContent("{\"name\": \"John\"}")
.WithHeader("Authorization", "Bearer token")
.WithTimeout(7500)
.Build();
// Create a POST Request Handler with Multipart form data with file
IRequestHandle request = Request.Post()
.WithUrl("https://api.example.com/upload".ToUri())
.WithMultipartContent()
.WithStreamData("file", "path/to/file.jpg", "image.jpg")
.WithContent("description", "Profile photo")
.Build();
// Send the request and get reponse
JToken response = await request.Send();
// Send the request and receive specified type of data
YourData response = await request.Send<YourData>();
// Cancel the request
request.Cancel();
// Dispose the request
request.Disposal();
// Get result from finished request
JToken result = request.Result;Exception Handling
IRequestHandle will throw serval exception types on catching errors.
A Task will be return on calling Send(). It's recommended to work with Promise for thread safe.
WebException
request content error
HttpRequestException
(500) server errors
(429) rate limiting
(4xx) other http errors
JsonReaderException
response content is not JSON format
OperationCanceledException
user cancel the request
Exception
retry on other exception
throw on reaching retry limit
WebRequesting()
.Then(OnSuccess)
.Catch<WebException>(OnConnectionError)
.Catch<HttpRequestException>(OnRequestError)
.Catch<JsonReaderException>(OnResponseError)
.Catch<OperationCanceledException>(OnUserCanceled)
.Catch(Debug.LogError)
.Final(() => Debug.Log("WebRequesting: Finished"));
private async Task WebRequesting()
{
var url = "https://www.orangeisbetterthenapple.io";
var request = Request.Get()
.WithUrl(url.ToUri())
.WithHeader("User-Agent", "MyUnityApp/1.0")
.Build();
var response = await request.Send();
Debug.Log(response);
}Sample of Weather Widget
For example, there is a Weather Widget on canvas. It will refresh every set time.
using System.Threading;
using System.Threading.Tasks;
using AceLand.TaskUtils;
using AceLand.WebRequest;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class TestWeatherWidget : MonoBehaviourr
{
[Header("Weather Widget")]
// WeatherProfile contains sprite of weather state
[SerializeField] private WeatherProfile profile;
// HongKongLocations contains districts and coordinates
[SerializeField] private HongKongLocations location = HongKongLocations.CentralAndWestern;
[SerializeField] private int updateInterval = 180;
[SerializeField] private Image weatherIcon;
[SerializeField] private TextMeshProUGUI tempLabel;
[SerializeField] private TextMeshProUGUI apparentTempLabel;
// setup API
private const string LATITUDE_KEY = "{latitude}";
private const string LONGITUDE_KEY = "{longitude}";
private const string API = "https://api.open-meteo.com/v1/" +
"forecast?latitude=" + LATITUDE_KEY +
"&longitude=" + LONGITUDE_KEY +
"¤t=temperature_2m,weather_code,rain,cloud_cover,showers,apparent_temperature,is_day" +
"&timezone=auto" +
"&forecast_days=1";
private Promise _promise;
private void OnEnable()
{
RequestWebData();
}
private void OnDisable()
{
_promise?.Dispose();
}
private void UpdateWeather(WeatherWidgetData data)
{
weatherIcon.sprite = profile.GetIcon(data.WeatherCode, data.IsDay);
tempLabel.text = data.Temperature;
apparentTempLabel.text = $"Apparent: {data.ApparentTemperature}";
}
private void RequestWebData()
{
var (latitude, longitude) = location.ToGeoData();
Uri api = API.Replace(LATITUDE_KEY, latitude.ToString("F4"))
.Replace(LONGITUDE_KEY, longitude.ToString("F4"))
.ToUri();
// create request
IRequestHandle request = Request.Get()
.WithUrl(api)
.Build();
// send request
_promise = request.Send<WeatherWebData>()
.Then(result =>
{
// convert data
WeatherWidgetData widgetData = result.ToWidgetData(location);
// use data
UpdateWeather(widgetData);
})
.Catch<WebException>(e =>
Debug.LogError($"connection error: {e.Message}", this))
.Catch<HttpRequestException>(e =>
Debug.LogError($"request error: {e.Message}", this))
.Catch<JsonReaderException>(e =>
Debug.LogError($"error on extracting data from server: {e.Message}", this))
.Catch<OperationCanceledException>(e =>
Debug.LogWarning("request canceled by user.", this))
.Catch(e =>
Debug.LogWarning($"other exception: {e}", this))
.Final(() =>
{
// prepare next call
WaitForRefresh();
// dispose current request
request.Dispose();
});
}
private void WaitForRefresh()
{
_promise = Promise.WaitForSeconds(updateInterval)
.Then(RequestWebData);
}
}Advanced Sample
This is a sample of building a User Authorization API serivce and how to use it.
using System.Threading.Tasks;
using AceLand.WebRequest;
// create a base class containing main urls
public abstract class BaseAPI
{
// reference to Api Url in Project Setting
private protected static string APIUrl => Request.ApiUrl;
}
// create UserAPI Service
public sealed class UserAPI : BaseAPI
{
// prepare all api urls.
private const string BASIC_AUTH_URL = "/basic-auth/{user}/{passwd}";
private static IRequestHandle _request;
// create a Promise to disposal request handle on finished
public static Promise<UserData> BasicAuth(string user, string password)
{
// combine a complete URL
string url = APIUrl + BASIC_AUTH_URL
.Replace("{user}", user)
.Replace("{passwd}", password);
// build Request Handle
_request = Request.Get()
.WithUrl(url.ToUri())
.Build();
// await response with specified type
return _request.Send<UserData>()
.Final(_request.Disposal);
}
public static async CancelBasicAuth()
{
_request?.Cancel();
}
}using AceLand.Library.Optional;
// Event for User Login
public interface IUserLoginEvent { }
public interface IUserCancelLoginEvent { }
public readonly struct UserLoginData : IValidatable
{
public readonly string User;
public readonly string Password;
private readonly UserLoginDataValidator _validator;
// use Optional for null handling
public UserLoginData(Option<string> user, Option<string> password)
{
User = user.Map(t => t.Trim()).Reduce(() => string.Empty);
Password = password.Map(t => t.Trim()).Reduce(() => string.Empty);
_validator = new UserLoginDataValidator();
}
// Validate User Login Data
public bool Validate() => _validator.Validate(this);
}
// Event for User Authorization Service
public interface IUserAuthEvent { }
public enum UserAuthState
{
None, Authorized, WrongUserPasswd,
ConnectionError, RequestError, ResponseError, AuthError
}using AceLand.Library.Extensions;
public interface IValidatable
{
bool Validate();
}
public abstract class Validator<T> where T : IValidatable
{
public virtual bool Validate(T data) => false;
}
public sealed class UserLoginDataValidator : Validator<UserLoginData>
{
private const int USERNAME_MIN_LENGTH = 8;
private const int PASSWORD_MIN_LENGTH = 8;
public override bool Validate(UserLoginData data) =>
!data.User.IsNullOrEmptyOrWhiteSpace() &&
!data.Password.IsNullOrEmptyOrWhiteSpace() &&
data.User.Length > USERNAME_MIN_LENGTH &&
data.Password.Length > PASSWORD_MIN_LENGTH;
}using AceLand.Library.Optional;
using UnityEngine;
public class UiLoginHandle : MonoBehaviour
{
// use Optional for null handling
private Option<string> _username;
private Option<string> _password;
// link to Input Component
public void OnUsernameInput(string value) =>
_username = value.ToOption();
// link to Input Component
public void OnPasswordInput(string value) =>
_password = value.ToOption();
// link to Button Component
public void OnLoginButtonPressed()
{
// create data
UserLoginData data = new UserLoginData(_username, _password);
// raize event
EventBus.Event<IUserLoginEvent>()
.WithSender(this)
.WithData(data)
.Raise();
}
public void OnCancelButtonPressed()
{
_username = string.Empty;
_password = string.Empty;
// raize event
EventBus.Event<IUserCancelLoginEvent>()
.WithSender(this)
.WithData(data)
.Raise();
}
}Last updated