Build a Better Unity HTTP Client
Author: Eric_Lian
TIP
If you are using Unity 6000.5 or later, you can use Unity Networking.UnityHttpMessageHandler instead of this custom implementation.
See Unity Document.
Abstract
HTTP is a great important protocol for communication between services. So that Unity provided UnityWebRequest class to make HTTP requests. However, UnityWebRequest is hard to use in certain scenarios. We all also know that .NET provides HttpClient class that is more powerful and flexible than UnityWebRequest, but according to Unity community's discussion, IL2CPP's HttpClient may hang when its is processing requests in some circumstances.
This blog post how Milthm team deal with those problem when using HTTP in Unity. What we did, why we did that, and how we solve the problems.
Background
Milthm introduce HTTP protocol since Nov 22 2023, but it used UnityWebRequest under the hood. To be honest, UnityWebRequest is quite hard to use, more specifically, it is hard to formulate a good request processing pipeline to provide services to upper business layers.
So, another commonly used library is RestSharp, which is a popular HTTP client library for .NET. It provides a simple and intuitive API for making HTTP requests and handling responses. And Milthm migrated to use RestSharp since Milthm 1.10.0, and we maintain a Unity supporting RestSharp fork that keeps differences from the original to a minimum.
It sounds nice, but there are some problems we encountered when using RestSharp in Unity.
Compatibility
After using RestSharp for a while, some users reported that they can not open Milthm, it just stuck at the loading page infinitely with no error message.
It shocked me, because when I built Milthm's networking stack, I followed all common client development practices, such as setting request timeouts and handling all possible exceptions. So it should not hang.
using var client = new RestClient(configureRestClient: cfg =>
{
cfg.FollowRedirects = false;
cfg.MaxTimeout = 2000;
});
var req = new RestRequest("https://google.com");
var resp = await client.ExecuteAsync(req);Unfortunately, no one can figure out how to reproduce. In other words no condition is certain. The only way to resolve this problem is just restart the game, restart the device network, change the network environment (from WLAN to cellular data, or switch to another WiFi), restart the device, etc. But none of these methods is reliable.
From them, a long-lasting war between Milthm and HTTP requests began.
Attempts
This problem is very hard to reproduce. I only spotted this problem once when I tried to find out what was going on.
The first time I spotted this problem was when I was testing Milthm on my laptop, I use the debugger to step through the whole procedure. After step into ExecuteAsync line, I found that code hang in RestSharp's code. But another weird phenomenon is the hanging function was wrapped with a CancellationToken, and the whole procedure written in RestSharp is quiet correct. There is no possibility to stuck in this procedure. So I guess it may be a problem of RestSharp or Unity's IL2CPP runtime, or some bugs in .Net's network library.
Due to Milthm is not great demand on HTTP client features in 2024, I decided to ignore this problem temporarily and just bumps the RestSharp version when a new version is released. And we also try our best to catch up with the latest version of Unity.
Investigation
It's a pity. This problem still exists in August 2025. And I have no choice but to resolve this problem now.
Because Milthm started to provide more online features since 4.0.0. Including user accounts, cloud game process, song redeeming etc. If these features are not working properly, it will greatly affect user experience. So I have to find a way to solve this problem.
After some investigation, I mapped out the runtime architecture of RestSharp environment. Restsharp is a wrapper of HttpClient. And HttpClient will select a HttpMessageHandler to process the request depending on the platform.
flowchart TB
App[Application Code]
App --> RestSharp
RestSharp --> RestClient
RestClient --> HttpClient
HttpClient --> HttpMessageHandler
HttpMessageHandler -->|Non-Browser| SocketsHttpHandler
HttpMessageHandler -->|Browser / WASM| BrowserHttpHandler
SocketsHttpHandler --> OSStack[OS Networking Stack]
BrowserHttpHandler --> BrowserAPI[Browser Networking APIs]
subgraph RS[RestSharp Layer]
RestClient
end
subgraph NET[System.Net.Http]
HttpClient
HttpMessageHandler
end
subgraph PLATFORM[Platform Specific Handlers]
SocketsHttpHandler
BrowserHttpHandler
endFrom the diagram above, we can observe that HttpClient uses different HttpMessageHandler depending on the platform. So an idea came to my mind, what if we replace the HttpMessageHandler with a custom one that uses UnityWebRequest under the hood?
Step deeper, I found that HttpClient can be configured with a custom HttpMessageHandler through its constructor. So implementing a custom HttpMessageHandler that uses UnityWebRequest is feasible.
Going further, I found that HttpMessageHandler is a very simple delegate to process HTTP requests. It only requires us to implement the SendAsync method that takes a HttpRequestMessage and returns a HttpResponseMessage.
// https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Net.Http/src/System/Net/Http/HttpMessageHandler.cs
namespace System.Net.Http
public abstract class HttpMessageHandler : IDisposable
{
protected internal abstract Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
}It is a great news! It means we can implement a custom HttpMessageHandler that uses UnityWebRequest to process HTTP easily.
Implementation
So here is the implementation of UnityHttpMessageHandler that uses UnityWebRequest under the hood.
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks; // UniTask
using UnityEngine.Networking; // Unity Networking
public class UnityWebRequestHandler : HttpMessageHandler
{
public class Option
{
public int MaxRedirects { get; set; } = 32;
}
private readonly Option _option;
public UnityWebRequestHandler(Action<Option> option)
{
_option = new Option();
option.Invoke(_option);
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
await UniTask.SwitchToMainThread(cancellationToken);
using var uwr = await CreateUnityWebRequest(request);
// Process the request
var operation = uwr.SendWebRequest();
// Support CancellationToken
while (!operation.isDone)
{
if (cancellationToken.IsCancellationRequested)
{
uwr.Abort();
throw new TaskCanceledException();
}
await Task.Yield();
}
// After request is done, construct HttpResponseMessage
// Construct HttpResponseMessage
var response = new HttpResponseMessage((HttpStatusCode)uwr.responseCode);
// Respons Body
if (uwr.downloadHandler != null)
{
var bytes = uwr.downloadHandler.data ?? Array.Empty<byte>();
response.Content = new ByteArrayContent(bytes);
}
// Clone Headers
foreach (var kv in uwr.GetResponseHeaders())
{
response.Headers.TryAddWithoutValidation(kv.Key, kv.Value);
}
// Finish Response
response.RequestMessage = request;
response.ReasonPhrase = uwr.error;
return response;
}
/// <summary>
/// Construct Unity Web Request
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private async Task<UnityWebRequest> CreateUnityWebRequest(HttpRequestMessage request)
{
UnityWebRequest uwr;
// Method
var method = request.Method.Method;
// Constuct UnityWebRequest instance and set body
if (request.Content != null)
{
var body = await request.Content.ReadAsByteArrayAsync();
uwr = new UnityWebRequest(request.RequestUri, method)
{
uploadHandler = new UploadHandlerRaw(body),
downloadHandler = new DownloadHandlerBuffer()
};
}
else
{
uwr = new UnityWebRequest(request.RequestUri, method)
{
downloadHandler = new DownloadHandlerBuffer()
};
}
// Copy Settings
uwr.redirectLimit = _option.MaxRedirects;
// Copy Headers
foreach (var header in request.Headers)
{
foreach (var value in header.Value)
{
uwr.SetRequestHeader(header.Key, value);
}
}
if (request.Content != null)
{
foreach (var header in request.Content.Headers)
{
foreach (var value in header.Value)
{
uwr.SetRequestHeader(header.Key, value);
}
}
}
return uwr;
}
}With this UnityWebRequestHandler, we can create a HttpClient instance that uses UnityWebRequest to process HTTP requests.
// Create UnityWebRequestHandler Instance
using var handler = new UnityWebRequestHandler((it) =>
{
it.MaxRedirects = 0;
});
// Create HttpClient Instance
using var httpClient = new HttpClient(handler);
// If you need to use RestSharp, you can create RestClient like this
var restClient = new RestClient(httpClient, false,
configureRestClient: cfg =>
{
cfg.FollowRedirects = false;
// Note: This setting is useless, it will not apply to HttpClient
// Reason is seen below
// https://github.com/restsharp/RestSharp/blob/261d94eba7e6ddd2ed59a17f420809d1346e92e7/src/RestSharp/RestClient.cs#L93
// https://github.com/restsharp/RestSharp/blob/261d94eba7e6ddd2ed59a17f420809d1346e92e7/src/RestSharp/RestClient.cs#L232
}
);In this way, we can use HttpClient in Unity without worrying about the hanging problem.
Conclusion
This feature is released in Milthm 4.4 as an experimental settings. Enabling a UnityWebRequest-based HttpMessageHandler reliably mitigates the intermittent HTTP hangs seen with IL2CPP/HttpClient. This approach has proven effective, as it resolved the issue across a wide range of devices.