Published on

Using UniRx To Perform Web Requests In Unity

Authors
  • avatar
    Name
    Gökhan Doğramacı
    Twitter

Table of Contents

Overview

There are multiple ways to perform web requests in Unity, one of which is using UnityEngine.WWW class. But this class is deprecated and the suggested way of making web requests is now UnityEngine.Networking.UnityWebRequest class.

This is a very handy class to perform GET, POST, PUT and DELETE requests. When you send a request, it returns UnityWebRequestAsyncOperation which you can use in multiple ways:

  • In a coroutine where you can yield it
  • In an async method where you can await it
  • As an IObservable where you can subscribe to it

Remember, it always depends on the situation to decide which one of these solutions is suitable for you. You can employ the Coroutine approach for simple use cases, even though they are a bit problematic. Or you can use the async / await approach for the cases where you need to return a value easily, or catch errors. Using the IObservable approach on the other hand may seem to be an overkill for some, but UniRx can be very powerful to perform the web requests considering its all benefits. Shortly, some of these are:

  • Being able to publish multiple values in observable streams.
  • Being able to easily catch errors, or format them to re-propagate.
  • Being able to use all available extension methods such as Where, Select, SelectMany / ContinueWith, Merge, Concat etc. to filter, format and combine multiple streams to create extremely easy-to-read flows.

In this article, we will learn how to perform web requests in play-mode and edit-mode with UniRx in Unity.

Web Requests in Play-Mode

With Coroutines in Play-Mode

Normally, we perform the requests like this with the coroutines:

private void Start()
{
    var coroutine = StartCoroutine(GetRequest("https://www.example.com"));

    // When we need to cancel it
    StopCoroutine(coroutine);
}

IEnumerator GetRequest(string uri)
{
    using (UnityWebRequest webRequest = UnityWebRequest.Get(uri))
    {
        // Request and wait for the desired page.
        yield return webRequest.SendWebRequest();

        // ~~~ Process the result ~~~
    }
}

Example is partially taken from the Unity documentation

This requires you to yield the request or listen to the operation and check IsDone property to understand if it's completed. In both of the situations, you cannot surround the yield statement with a try-catch block, and you cannot return a value from the coroutine as the return type must be IEnumerator. And don't forget, you need this coroutine script to be placed in one of your GameObjects, and make sure that it is always alive for it to be executed successfully.

With UniRx in Play-Mode

Luckily, with UniRx, we don't have the limitations above, and we don't need to manage a GameObject to execute the coroutine. Not only that, we will be able to access many useful extension methods. Here's an example:

private void Start()
{
    var requestDisposable = UnityWebRequest
        .Get("https://www.example.com")
        .SendWebRequest()
        .AsAsyncOperationObservable()
        .Subscribe(result =>
        {
            // ~~~ Process the result ~~~
        });

        // When we need to cancel it
        requestDisposable.Dispose();
}

We can easily convert a web request call (UnityWebRequestAsyncOperation) to an observable stream with the AsAsyncOperationObservable extension method. And whenever we need to cancel it, disposing of the return value of the subscription should do the trick.

Cancelling the subscription

Canceling the subscription does not abort the request but disposes of the request object to release its used resources. Most of the time, you do not need to Abort the request, but if it is the intended behavior, it can be implemented in a helper or extension method with Observable.Create method.

Web Requests in Edit-Mode

With Coroutines in Edit-Mode

You can still use the Coroutines to perform the web requests in Edit-Mode. For example, the EditorCoroutineUtility helper class can be used to start the coroutines whenever you need to call an endpoint in an Editor window.

public class ExampleWindow : EditorWindow
{
    void OnEnable()
    {
        var url = "https://www.example.com";
        var coroutine = EditorCoroutineUtility.StartCoroutine(GetRequest(url), owner: this);

        // When we need to cancel it
        EditorCoroutineUtility.StopCoroutine(coroutine);
    }

    IEnumerator GetRequest(string uri)
    {
        using (UnityWebRequest webRequest = UnityWebRequest.Get(uri))
        {
            // Request and wait for the desired page.
            yield return webRequest.SendWebRequest();

            // ~~~ Process the result ~~~
        }
    }
}

Example is partially taken from the Unity documentations

With this approach, coroutine execution is delegated to some other object (EditorCoroutine class that listens to EditorApplication.Update) but this doesn't remove the limitations of the coroutines.

With UniRx in Edit-Mode

Listening to the web requests with UniRx in Edit-Mode is very similar to doing it in Play-Mode. We can still use the all benefits of reactive extensions once we convert the async operation to an observable stream.

public class ExampleWindow : EditorWindow
{
    void OnEnable()
    {
        var requestDisposable = UnityWebRequest
            .Get("https://www.example.com")
            .SendWebRequest()
            .AsAsyncOperationObservable()
            .Subscribe(result =>
            {
                // ~~~ Process the result ~~~
            });


        // When we need to cancel it
        requestDisposable.Dispose();
    }
}

However, by the time of writing this blog post, there's a nasty issue in UniRx code preventing listening to these requests in Edit-Mode as expected. You can see this issue I created in UniRx repository to see the details about this issue and how to solve it.

There is a couple of ways to listen to the request as expected until the issue is fixed.

1. Providing an IProgress implementation as parameter

Even though you may not need the progress feedback, providing this parameter forces UniRx to check IsDone property of the async operation, hence it works as expected.

var requestDisposable = UnityWebRequest
    .Get("https://www.example.com")
    .SendWebRequest()
    .AsAsyncOperationObservable(new Progress<float>())
    .Subscribe(result =>
    {
        // ~~~ Process the result ~~~
    });

2. Using the fixed package

I already merged the fix in my forked repository. It contains a few improvements and fixes alongside this fix. You can import it to your project instead of the original UniRx package for it to work as expected:

https://github.com/dogramacigokhan/UniRx.git?path=Assets/Plugins/UniRx/Scripts#gdfixes

Examples

From the codes above, it may look like there's no major difference between using coroutines and UniRx. But the difference becomes visible when we start using them in real-life scenarios. I won't be giving examples for the Coroutine counterparts, but only UniRx example. I think that would be enough to understand the point.

Waiting for list of requests:

private IObservable<UnityWebRequestAsyncOperation> Get(string url) =>
    UnityWebRequest.Get(url).SendWebRequest().AsAsyncOperationObservable();

private void Start()
{
    // Inform when all of the requests are completed
    var parallel = Observable.WhenAll(
        Get("http://google.com/"),
        Get("http://bing.com/"),
        Get("http://unity3d.com/"));

    parallel.Subscribe(xs =>
    {
        Debug.Log(xs[0]); // google
        Debug.Log(xs[1]); // bing
        Debug.Log(xs[2]); // unity
    });
}

Making sequential requests

private IObservable<UnityWebRequestAsyncOperation> Get(string url) =>
    UnityWebRequest.Get(url).SendWebRequest().AsAsyncOperationObservable();

private void Start()
{
    var disposable = Get("https://www.example.com")
        .ContinueWith(operation =>
        {
            // ~~~ We can process the first response here and start the second call ~~~
            var response = operation.webRequest.downloadHandler.text;
            return Get("https://www.google.com");
        })
        .Subscribe(response => {
            // ~~~ We can process the final response here ~~~
        });

    // Disposing this will cancel the full sequence
    disposable.Dispose();
}

Sending request when input box changes

Let's see how can we perform a web request when an input box value changes by delaying the request until user stops typing for 200 milliseconds.

[SerializeField] private TMP_InputField searchBox;

private IObservable<UnityWebRequestAsyncOperation> Get(string url) =>
    UnityWebRequest.Get(url).SendWebRequest().AsAsyncOperationObservable();

private void Start()
{
    var disposable = this.searchBox
        .OnValueChangedAsObservable()
        .Throttle(TimeSpan.FromMilliseconds(200))
        .SelectMany(searchTerm => Get($"https://www.example.com/q={searchTerm}"))
        .Subscribe(response =>
        {
            // ~~~ We can process the response here ~~~
        });

    // Disposing this will cancel both search-box listening and the request listening
    disposable.Dispose();
}

Comprehensive helper method to make requests

public static IObservable<UnityWebRequest> Download(
    string url,
    Func<string, UnityWebRequest> webRequestFunc,
    Dictionary<string, string> headers = null)
{
    return Observable.Create<UnityWebRequest>(observer =>
    {
        var request = webRequestFunc(url);
        var requestDisposable = new SingleAssignmentDisposable();

        // Set default header to accept json
        request.SetRequestHeader("Content-Type", "application/json");
        request.SetRequestHeader("Accept", "application/json");

        if (headers != null)
        {
            foreach (var header in headers)
            {
                request.SetRequestHeader(header.Key, header.Value);
            }
        }

        IObservable<UnityWebRequest> HandleResult()
        {
            if (requestDisposable.IsDisposed)
            {
                return Observable.Throw<UnityWebRequest>(
                    new OperationCanceledException("Already disposed."));
            }

            if (request.result != UnityWebRequest.Result.Success)
            {
                return Observable.Throw<UnityWebRequest>(new WebException(request.error));
            }

            if (request.responseCode != (long)HttpStatusCode.OK)
            {
                return Observable.Throw<UnityWebRequest>(
                    new WebException($"{request.responseCode} - {request.downloadHandler.text}"));
            }

            return Observable.Return(request);
        }

        requestDisposable.Disposable = request
            .SendWebRequest()
            .AsAsyncOperationObservable()
            .ContinueWith(_ => HandleResult())
            .CatchIgnore((OperationCanceledException _) => observer.OnCompleted())
            .Subscribe(result =>
            {
                observer.OnNext(result);
                observer.OnCompleted();
            });

        return new CompositeDisposable(request, requestDisposable);
    });
}

With this Download method, we can:

  • Make sure the lifetime of the request is handled properly
  • Publish value(s) when the request completes successfully
  • Publish error when the request fails for any reason
  • Use it for a different type of UnityWebRequests
  • Filter, reshape or combine its result with other observable streams to create complex but easy-to-follow flows.

For example, to perform a GET request:

Download("https://www.example.com", UnityWebRequest.Get).Subscribe(result => { /* ~~~ Process the result ~~~ */ })

To perform a POST request:

Download("https://www.example.com", uri => UnityWebRequest.Post(uri, "sample-post-data")).Subscribe()

There are many more extension methods that you can use with UniRx to reshape your data and combine multiple streams to simplify complex flows. Check out RxMarbles to see how some of the extension methods work in action.