> For the complete documentation index, see [llms.txt](https://docs.parsue.io/aceland-unity-packages/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.parsue.io/aceland-unity-packages/packages/webrequest.md).

# WebRequest

## 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

<table data-header-hidden><thead><tr><th width="174"></th><th></th></tr></thead><tbody><tr><td>display name</td><td>AceLand WebRequest</td></tr><tr><td>package name</td><td><pre><code>com.aceland.webrequest
</code></pre></td></tr><tr><td>latest version</td><td>2.5.0</td></tr><tr><td>namespace</td><td><pre><code>AceLand.WebRequest
</code></pre></td></tr><tr><td>git repository</td><td><a href="https://github.com/parsue/com.aceland.webrequest.git">https://github.com/parsue/com.aceland.webrequest.git</a></td></tr><tr><td>dependencies</td><td><pre><code>com.aceland.library: 2.2.1
com.aceland.disposable: 1.0.0
com.aceland.eventdriven: 2.5.1
com.aceland.playerloophack: "1.2.1"
com.aceland.projectsetting: 1.0.0
com.aceland.serialization: 1.1.4
com.aceland.taskutils: 2.2.5
com.unity.nuget.newtonsoft-json: 3.2.1
</code></pre></td></tr></tbody></table>

***

## Why 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
* **Front-end Friendly**: Handling exception as what frontend thinking

***

## Key Features

* Custom retry intervals and timeout settings
* Comprehensive logging system with different build levels
* HTTPS enforcement option
* Automatic timestamp header injection
* Automatic use cookie (will be optional in next release)
* JSON validation before sending
* Support for cancellation tokens
* Editor tools for API section management

***

## Project Settings

<table data-header-hidden><thead><tr><th width="231"></th><th></th></tr></thead><tbody><tr><td><strong>Logging</strong></td><td>---</td></tr><tr><td>Logging Level</td><td>Level of Logging on web request<br>default: <code>BuildLevel.Production</code></td></tr><tr><td>Result Logging Level</td><td><p>Level of Logging on request success</p><p>default: <code>BuildLevel.DevelopmentBuild</code></p></td></tr><tr><td>Full Logging Level</td><td><p>Level of Logging on all request logs content.</p><p>Logs will contain all parameters, headers and body contents.<br>default: <code>BuildLevel.DevelopmentBuild</code></p></td></tr><tr><td><strong>Checking Options</strong></td><td>---</td></tr><tr><td>Check Json Before Send</td><td>Check given content is a valid json before send request.<br>This may be expensive if big content.<br>Default: <code>false</code></td></tr><tr><td>Force Https Scheme</td><td>Require request url is https if true. Otherwise http will also ok.<br>Default: <code>true</code></td></tr><tr><td><strong>Concurrent Options</strong></td><td>---</td></tr><tr><td>Max Concurrent Requests</td><td>Control max concurrent requests out of API sections profile.<br>Default: <code>10</code>   (<code>0</code> for unlimit)</td></tr><tr><td><strong>Request Options</strong></td><td>---</td></tr><tr><td>Request Timeout</td><td>Default timeout if not set in request handle.<br>Default: <code>3000</code> ms</td></tr><tr><td>Long Request Timeout</td><td>Default long request timeout.  It will be set if building request handle with long request.<br>Default: <code>15000</code> ms</td></tr><tr><td>Request Retry</td><td>How many times will retry on connection error including timeout.<br>Default: <code>3</code></td></tr><tr><td>Retry Interval</td><td>Interval between each retry.<br>Default: <code>[ 400, 800, 1600, 3200, 6400, 12800, 25600]</code> in ms</td></tr><tr><td><strong>Header Auto Fill</strong></td><td>---</td></tr><tr><td>Add Time In Header</td><td>Add send time in header automatically.<br>Default: <code>true</code></td></tr><tr><td>Time Key</td><td>key of Time in header.<br>default: <code>Time</code></td></tr><tr><td>Auto Fill Headers</td><td>Add default headers on each Request.<br>Same headers will be covered by WithHeaders options on building Request.<br>Default: <code>{ "User-Agent", "Mozilla/5.0" }</code></td></tr><tr><td><strong>API Sections Profiles</strong></td><td>---</td></tr><tr><td>Api Sections (c)</td><td><p>This is a list of API Sections Profiles.<br>title bar: <code>+</code> add new field   <code>-</code> delete last field</p><p>in line:  <code>-</code> delete field   <code>*</code> apply to Current API Section<br><br>** not built</p></td></tr><tr><td><strong>Default API Section</strong></td><td>---</td></tr><tr><td>Section Name</td><td>[Lock] default API section.</td></tr><tr><td>Api Url</td><td>[Lock] API URL of default section.</td></tr><tr><td><code>Clear Current Section</code></td><td>clear Default API Section setting.</td></tr></tbody></table>

***

## API

```csharp
// base API
Request

// Value
string defaultSectionName = Request.DefaultSection;
string defaultApiUrl = Request.DefaultApiUrl;
string apiUrl = Request.SectionApiUrl("sectionName");
IEnumerable<HeaderData> defaultHeaders = Request.DefaultHeaders;
IEnumerable<HeaderData> header = Request.SectionHeaders("sectionName");
```

***

## API Section

API section is a profile built with scriptable object.&#x20;

1. Create a API Section Profile in Project Window. it's recommended to hold all profiles in single folder.

<figure><img src="/files/oA2OkDvZkBvn9kXEUHXF" alt="" width="331"><figcaption></figcaption></figure>

2. Input necessary information for the section
   * `Section Name` : \[readonly] set as profile name
   * `API Domain` : set the base url of this section
   * `Max Concurrent Request` : set max concurrent for this section
   * `Private Root CA` : \[readonly]
     * set : input full path to Pem File and press `Invoke`
     * remove : press `Remove Root CA`

{% hint style="success" %}
about creating and setting private CA, please refer following document.

```
https://docs.parsue.io/dot-net-notes/web-api/private-certificate
```

{% endhint %}

3. In Project Settings, find API Sections Profiles section, and press `+` to add a new row.\
   Add more sections as your needs.

<figure><img src="/files/HWFhD0n5GovdLVfVP0dV" alt=""><figcaption></figcaption></figure>

4. Press `*` to set default. the default section will show in the below section.

<figure><img src="/files/VoaIHb6OuQgBJBSAWJ3p" alt="Default API Section"><figcaption></figcaption></figure>

***

## Concurrent Request

This package can limit concurrent requests on same domain.  `Max Concurrent Request` can be set in Project Settings and API Sections Project.

Concurrent Requests will be limited to requests not from API Sections Profile.  This will still count by domain name in url.

If the API Section is using url as `www.mydomain.com` and there is a requests built without using the section but same domain, it will still count in same concurrent.

***

## Basic Usage

{% tabs %}
{% tab title="Build Request" %}
Building request is easy.

* start with request method
* chain with url first
* add parameters if necessary
* add Header if necessary, see [Project Settings](#project-settings)
* add Body if necessary, see [Request Body](#request-body)

```csharp
// Create a GET Request Handler
// default section url: https://api.abc.com
// request url: https://api.example.com/data?clientid:a123456
IRequestHandle request = Request.Get()
    .WithoutSection()
    .WithUrl("https://api.example.com/data")
    .WithParam("clientid", "a123456")
    .WithHeader("User-Agent", "MyUnityApp/1.0")
    .WithHeader("API-KEY", "abcde-5d8d1fe")
    .Build();
    
// Create a POST Request Handler
// default section url: https://api.abc.com
// request url: https://api.abc.com/users?clientid:a123456
IRequestHandle request = Request.Post()
    .WithUrl("/users")    // combine with default section url
    .WithParam("clientId", "a123456")
    .WithJsonContent()
    .WithContent("{\"name\": \"John\"}")
    .WithHeader("Authorization", "Bearer token")
    .WithTimeout(7500)
    .Build();
```

{% endtab %}

{% tab title="Send Request" %}
After created a request, call Send() method.

```csharp
// Send the request and get reponse
// if server return non json string,
//    it will become { "message" : "server response" }
JToken response = await request.Send();
    
// Send the request and receive specified type of data
YourData response = await request.Send<YourData>();
```

It's recommended to work with [Promise](/aceland-unity-packages/packages/task-utils.md) for thread and exception safe.

```csharp
request.Send()
    .Then(OnJsonResponse)
    .Catch(Debug.LogError);
    
request.Send<YourData>()
    .Then(OnSuccess)
    .Catch(Debug.LogError);
    
private void OnJsonResponse(JToken response) { }
private void OnSuccess(YourData response) { }
```

{% endtab %}

{% tab title="Other" %}

```csharp
// Cancel the request
request.Cancel();

// Dispose the request
request.Disposal();

// Get result from finished request
JToken result = request.Result;
```

{% endtab %}
{% endtabs %}

***

## Request Body

In real-world experience, servers, proxies or clients will ignore body on methods even allowed by spec.

For a promise of communictaion, our request handle will follow the modern experience.

<table><thead><tr><th>Method</th><th data-type="checkbox">Json Body</th><th data-type="checkbox">Multipart Body</th><th data-type="checkbox">Form Body</th></tr></thead><tbody><tr><td>Get</td><td>false</td><td>false</td><td>false</td></tr><tr><td>Post</td><td>true</td><td>true</td><td>true</td></tr><tr><td>Put</td><td>true</td><td>true</td><td>true</td></tr><tr><td>Patch</td><td>true</td><td>true</td><td>true</td></tr><tr><td>Delete</td><td>true</td><td>true</td><td>true</td></tr></tbody></table>

{% hint style="warning" %}
About Delete method, please avoid sending a body. Many servers ignore it. Semantics are unclear.
{% endhint %}

***

## Exception Handling

`IRequestHandle` will throw serval exception types and event on catching errors in frontend thinking method.

For backend/http request thinking, a request success is that sent and returning message from server before timeout, even it's returning `400 Bad Request`, `401 Unauthorized` etc. And also, an error is just an error, server will throw en message with http status only because backend is database goalkeeper.

However frontend is end-user goalkeeper.  Only `2xx` status are green, all other status are error.  And also some of the status are dev-level error, frontend developers must solve all those errors before deploying the prod-build.

As an Unity package, `IRequestHandle` handles status in frontend method.

Errors related to connection and server errors, and also unauthorized will raise a global event.  Frontend can catch the event and handle the app flow in single place.

By separating all `4xx` status to exceptions, frontend developers can handle the problem in catch block but not try block.

{% hint style="warning" %}
A Task will be returned on calling `Send()`. It's recommended to work with Promise for thread safe.
{% endhint %}

<table><thead><tr><th width="248.2000732421875">Exception Type</th><th width="230.60009765625">Description</th><th width="69.1998291015625" data-type="checkbox">Retry</th><th width="209.5999755859375">Event</th></tr></thead><tbody><tr><td><code>FileNotFoundException</code></td><td><ul><li>file not exists in multipart</li></ul></td><td>false</td><td></td></tr><tr><td><code>InvalidDataException</code></td><td><ul><li>invalide data in multipart</li></ul></td><td>false</td><td></td></tr><tr><td><code>WebException</code></td><td><ul><li>connection error</li></ul></td><td>true</td><td><code>IConnectionErrorEvent</code></td></tr><tr><td><code>ServerErrorException</code></td><td><ul><li>≥ 500 all server errors</li><li>429 too many requests</li></ul></td><td>false</td><td><code>IServerErrorEvent</code></td></tr><tr><td><code>BadRequestException</code></td><td><ul><li>400 Bad Request</li></ul></td><td>false</td><td></td></tr><tr><td><code>UnauthorizedException</code></td><td><ul><li>401 Unauthorized</li></ul></td><td>false</td><td><code>IUnauthorizedEvent</code></td></tr><tr><td><code>ForbiddenException</code></td><td><ul><li>403 Forbidden</li></ul></td><td>false</td><td></td></tr><tr><td><code>NotFoundException</code></td><td><ul><li>404 Not Found</li></ul></td><td>false</td><td></td></tr><tr><td><code>ConflictException</code></td><td><ul><li>409 Conflict</li></ul></td><td>false</td><td></td></tr><tr><td><code>HttpErrorException</code></td><td><ul><li>other http errors</li></ul></td><td>false</td><td></td></tr><tr><td><code>HttpRequestException</code></td><td><ul><li>unexpected http errors</li></ul></td><td>false</td><td></td></tr><tr><td><code>JsonException</code></td><td><ul><li>non json response content</li><li>json serialize fail to specified type</li></ul></td><td>false</td><td></td></tr><tr><td><code>OperationCanceledException</code></td><td><ul><li>request canceled</li><li>request timeout</li></ul></td><td>false</td><td></td></tr><tr><td><code>Exception</code></td><td><ul><li>throw on reaching retry limit</li></ul></td><td>false</td><td></td></tr><tr><td>other known exceptions</td><td><ul><li>develop level errors</li></ul></td><td>false</td><td></td></tr><tr><td>other unknown exceptions</td><td><ul><li>unexpected task cancelation</li></ul></td><td>false</td><td></td></tr></tbody></table>

```csharp
WebRequesting()
    .Then(OnSuccess)
    .Catch<WebException>(OnConnectionError)
    .Catch<HttpServerErrorException>(OnServerError)
    .Catch<HttpErrorException>(OnHttpError)
    .Catch<OperationCanceledException>(OnUserCanceled)
    .Catch(Debug.LogError)
    .Final(() => Debug.Log("WebRequesting: Finished"));

private async Task<ResponseData> WebRequesting()
{
    var url = "https://www.orangeisbetterthenapple.io";
    var request = Request.Get()    // define method
        .WithUrl(url)
        .WithHeader("User-Agent", "MyUnityApp/1.0")
        .Build();
    return request.Send<ResponseData>(); 
}

private void OnSuccess(ResponseData response)
{
    // stuff
}

private void OnHttpError(HttpErrorException e)
{
    var msg = e.StatusCode switch
    {
        HttpStatusCode.Unauthorized => "Auth Failed. Auth Token removed.",
        >= HttpStatusCode.InternalServerError => $"Server Error: - {e.Message}",
        HttpStatusCode.TooManyRequests => $"Server Busy: - {e.Message}",
        _ => $"Http Error: {e.Message}"
    };
    Debug.LogWarning(msg);
}
```

### Event Handling

`IRequestHandle` will raise event on catching global exceptions like connection error.  Application design can handle single event on the user flow easily.

Event will be raise without cache. Listen with kickstart will not received any old event in these events.

{% hint style="success" %}
Please read [EventBus](/aceland-unity-packages/packages/event-driven/event-bus.md) of [EventDriven](/aceland-unity-packages/packages/event-driven.md) for details.
{% endhint %}

```csharp
using System.Net;
using AceLand.EventDriven.Bus;
using AceLand.WebRequest.Events;
using AceLand.WebRequest.Exceptions;
using UnityEngine;

public class GlobalWebExceptionHandle : MonoBehaviour,
    IConnectionErrorEvent, IServerErrorEvent, IUnauthorizedEvent
{
    private void OnEnable()
    {
        EventBus.Event(this).Listen();
    }

    private void OnDisable()
    {
        EventBus.Event(this).Unlisten();
    }

    public void OnConnectionError(WebException webException)
    {
        Time.timeScale = 0;
        RetryPopupWarning(webException.Message);
    }

    public void OnServerError(ServerErrorException serverErrorException)
    {
        Time.timeScale = 0;
        RetryPopupWarning(webException.Message);
    }

    public void OnUnauthorized(UnauthorizedException unauthorizedException)
    {
        Time.timeScale = 0;
        LoginPopupWarning(unauthorizedException.Message);
    }
}
```

***

## Sample of Weather Widget

For example, there is a Weather Widget on canvas.  It will refresh every set time.

```csharp
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
    // default section url: https://api.open-meteo.com/v1
    private const string API = "/forecast";
   
    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();
        
        // create request
        IRequestHandle request = Request.Get()
            .WithUrl(API)
            .WithParam("latitude", latitude.ToString("F4"))
            .WithParam("longitude", longitude.ToString("F4"))
            .WithParam("current", "temperature_2m,weather_code,rain,cloud_cover,showers,apparent_temperature,is_day")
            .WithParam("timezone", "auto")
            .WithParam("forecast_days", "1")
            .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<HttpServerErrorException>(e =>
                Debug.LogError($"server error: {e.Message}", this))
            .Catch<HttpErrorException>(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);
    }
}
```

{% hint style="info" %}
Work with Promise Awaiter for thread-safe and clear coding structure.

Please read [Task Utils](/aceland-unity-packages/packages/task-utils.md) for details.
{% endhint %}

***

## Advanced Sample

This is a sample of building a User Authorization API serivce and how to use it. An API section profile is already set in project settings and set as default.

{% hint style="info" %}
Following library and packages are using in above example:

* [Library](/aceland-unity-packages/packages/library.md)
  * [Optional](broken://pages/vNCnYQKB0Z4Op43wPbnM)
  * [Singleton](broken://pages/ka5eu4srPx1shriDHwQO)
* [Event Driven](/aceland-unity-packages/packages/event-driven.md)
  * [EventBus](/aceland-unity-packages/packages/event-driven/event-bus.md)
* [Task Utils](/aceland-unity-packages/packages/task-utils.md)
  {% endhint %}

{% tabs %}
{% tab title="API Serivce" %}

```csharp
using System.Threading.Tasks;
using AceLand.WebRequest;

// create UserAPI Service
public sealed class UserAPI
{
    // prepare all api urls.
    private const string BASIC_AUTH_URL = "/basic-auth";
    
    private static IRequestHandle _request;
    
    // create a Promise to disposal request handle on finished
    public static Promise<UserData> BasicAuth(string user, string password)
    {
        // build Request Handle
        _request = Request.Get()
            .WithUrl(BASIC_AUTH_URL )
            .WithParam("user", user)
            .WithParam("Password", password)
            .Build();
        
        // await response with specified type
        return _request.Send<UserData>()
            .Final(_request.Disposal);
    }
    
    public static async CancelBasicAuth()
    {
        _request?.Cancel();
    }
}
```

{% endtab %}

{% tab title="Events and Data" %}

```csharp
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
}
```

{% endtab %}

{% tab title="Validator" %}

```csharp
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;
}
```

{% endtab %}

{% tab title="UserAuthorization" %}

```csharp
using System;
using AceLand.EventDriven.Bus;
using AceLand.Library.Mono;
using AceLand.TaskUtils;

public class UserAuthorization : Singleton<UserAuthorization>
{
    private UserAuthState _userAuthState;
    private UserData _userData;
    private bool _busy;
    
    private void OnEnable()
    {
        // Listen to events
        EventBus.Event<IUserLoginEvent>()
            .WithListener<UserLoginData>(OnUserLogin)
            .WithKickStart()
            .Listen();
            
        EventBus.Event<IUserCancelLoginEvent >()
            .WithListener(OnUserCancelLogin)
            .WithKickStart()
            .Listen();
    }
    
    private void OnDisable()
    {
        // Unlisten from IUserLoginEvent
        EventBus.Event<IUserLoginEvent>()
            .Unlisten<UserLoginData>(OnUserLogin);
            
        EventBus.Event<IUserCancelLoginEvent >()
            .Unlisten(OnUserCancelLogin);
    }

    private void OnUserLogin(object sender, UserLoginData data)
    {
        if (sender is not UiLoginHandle)
            throw new Exception($"Illegal User Auth Sender detected: {typeof(sender)}");
    
        // checking policies
        if (!data.Validate())
        {
            OnAuthEvent(UserAuthState.WrongUserPasswd);
            return;
        }
        
        // start user auth
        HandleUserAuth(data);
    }
    
    private void OnUserCancelLogin(object sender)
    {
        if (sender is not UiLoginHandle)
            throw new Exception($"Illegal User Auth Sender detected: {typeof(sender)}");
        
        // cancel user auth
        CancelUserAuth();
    }

    private void HandleUserAuth(UserLoginData data)
    {
        if (_busy) return;
        if (_userAuthState is UserAuthState.Authorized)
        {
            OnAuthSuccess(_userData);
            return;
        }
        
        _busy = true;
        
        // Use Promise Awaiter to handle request
        UserAPI.BasicAuth(data.User, data.Password)
            .Then(OnAuthSuccess)
            .Catch<WebException>(_ => OnAuthEvent(UserAuthState.ConnectionError))
            .Catch<HttpRequestException>(_ => OnAuthEvent(UserAuthState.RequestError))
            .Catch<JsonReaderException>(_ => OnAuthEvent(UserAuthState.ResponseError))
            .Catch<OperationCanceledException>(_ => OnAuthEvent(UserAuthState.AuthError))
            .Catch(_ => OnAuthEvent(UserAuthState.AuthError))
            .Final(OnAuthFinish);
    }
    
    private void CancelUserAuth()
    {
        if (!_busy) return;
        UserAPI.CancelBasicAuth();
    }

    private void OnAuthSuccess(UserData userData)
    {
        _userData = userData;
        OnAuthEvent(UserAuthState.Authorized);
    }

    private void OnAuthEvent(UserAuthState state)
    {
        _userAuthState = state;
        // raise event IUserAuthEvent
        EventBus.Event<IUserAuthEvent>()
            .WithSender(this)
            .WithData(state)
            .Raise();
    }

    private void OnAuthFinish()
    {
        _busy = false;
    }
}
```

{% endtab %}

{% tab title="UI User Login" %}

```csharp
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();
    }
}
```

{% endtab %}
{% endtabs %}
