YetAnotherHttpHandler brings the power of HTTP/2 to Unity and .NET Standard.
This library enables the use of HTTP/2, which Unity does not support. It allows you to use grpc-dotnet instead of the deprecated C-core gRPC library. It can also be used for asset downloading via HTTP/2, providing more functionality to your projects.
The library is implemented as a HttpHandler for HttpClient, so you can use the same API by just replacing the handler. (drop-in replacement)
- Unity support (including Apple Silicon support)
- Compatible with gRPC (grpc-dotnet)
- Leveraging
System.Net.Http.HttpClientAPI
The handler is built on top of hyper and Rustls. It is a binding library that brings the power of those libraries to Unity and .NET.
- Unity 2021.3 (LTS) or later
| Architecture/Platform | Windows | Windows (UWP) | macOS | Linux | Android | iOS | iOS (Simulator) |
|---|---|---|---|---|---|---|---|
| x64 (x86_64) | ✔ | - | ✔ | ☁ | ☁ | ☁ | - |
| arm64 (aarch64, Apple Silicon) | ☁ | ☁ | ✔ | 📆 | ✔ | ✔ | ☁ |
| armv7 | - | - | - | - | ☁ | - | - |
- ✔ (Tier 1): Verify that it works, and active development support.
- ☁ (Tier 2): The build is available, but not confirmed to work.
- 📆 (Planned): Currently, the build is not available yet.
- HTTP/1.0, HTTP/1.1
- HTTP/2
- Multiple streams on a single connection
- Compatible with grpc-dotnet (Grpc.Net.Client)
- HTTP/2 over cleartext
- TLS 1.2/1.3 with ALPN
- TLS support is powered by Rustls + webpki
- Client certificate
- Unix Domain Socket (macOS, Linux)
- HTTP proxy support
- Verification of certificates by security features built into the OS
- More platform supports
- Linux on Arm
- NTLM and Kerberos authentication
- Platforms
- Unity 2021.2 or earlier
- .NET 5+
- 32bit architectures (x86)
- tvOS, watchOS, Tizen
- WebGL
This library depends on the following additional libraries:
- System.IO.Pipelines (netstandard2.1)
- System.Runtime.CompilerServices.Unsafe (netstandard2.1)
- Install NuGetForUnity to your Unity project
- Install the following NuGet packages via NuGetForUnity:
- System.IO.Pipelines
- System.Runtime.CompilerServices.Unsafe
- Specify the following URL in
Add package from git URL...of Package Manager on Unity
https://github.com/Cysharp/YetAnotherHttpHandler.git?path=src/YetAnotherHttpHandler#{Version}
Note
Please replace {Version} with the version number you want to install (e.g. 1.8.0). See Releases page.
- Add the UnityNuget scope registry:
https://github.com/xoofx/UnityNuGet#add-scope-registry-manifestjson
- Specify the following URL in
Add package from git URL...of Package Manager on Unity. This is the package that resolves NuGet dependencies.
https://github.com/Cysharp/YetAnotherHttpHandler.git?path=src/YetAnotherHttpHandler.Dependencies#{Version}
- Specify the following URL in
Add package from git URL...of Package Manager on Unity. This is the main packagee.
https://github.com/Cysharp/YetAnotherHttpHandler.git?path=src/YetAnotherHttpHandler#{Version}
Note
Please replace {Version} with the version number you want to install (e.g. 1.8.0). See Releases page.
-
Please download and install Cysharp.Net.Http.YetAnotherHttpHandler.Dependencies.unitypackage from the dependency redistribution on the release page.
-
Specify the following URL in
Add package from git URL...of Package Manager on Unity.
https://github.com/Cysharp/YetAnotherHttpHandler.git?path=src/YetAnotherHttpHandler#{Version}
Note
Please replace {Version} with the version number you want to install (e.g. 1.8.0). See Releases page.
Create an instance of YetAnotherHttpHandler and pass it to HttpClient.
using Cysharp.Net.Http;
using var handler = new YetAnotherHttpHandler();
var httpClient = new HttpClient(handler);
var result = await httpClient.GetStringAsync("https://www.example.com");With these steps, your HttpClient is now compatible with HTTP/2.✨
YetAnotherHttpHandler and HttpClient can be held following best practices and shared across multiple threads or requests.
However, since it does not have features such as connection control by the number of streams, it is necessary to separate handler instances when explicitly creating different connections.
To use grpc-dotnet (Grpc.Net.Client), add the following additional libraries:
- Grpc.Core.Api
- Grpc.Net.Client
- Grpc.Net.Common
- Microsoft.Extensions.Logging.Abstractions
- System.Diagnostics.DiagnosticSource
- Install
Grpc.Net.Clientfrom NuGet via NuGetForUnity
To use grpc-dotnet (Grpc.Net.Client), add the following additional library to manifest.json:
{
"dependencies": {
"org.nuget.grpc.net.client": "{version}"
}
}Note
Replace {version} with the latest version available in NuGet: https://www.nuget.org/packages/Grpc.Net.Client#versions-body-tab
Please download and install Grpc.Net.Client.Dependencies.unitypackage from the dependency redistribution on the release page, or obtain the library from NuGet.
Create an instance of YetAnotherHttpHandler and pass it to GrpcChannelOptions.HttpHandler property.
using Cysharp.Net.Http;
using var handler = new YetAnotherHttpHandler();
using var channel = GrpcChannel.ForAddress("https://api.example.com", new GrpcChannelOptions() { HttpHandler = handler });
var greeter = new GreeterClient(channel);
var result = await greeter.SayHelloAsync(new HelloRequest { Name = "Alice" });
// -- OR --
using var channel = GrpcChannel.ForAddress("https://api.example.com", new GrpcChannelOptions() { HttpHandler = new YetAnotherHttpHandler(), DisposeHttpClient = true });
var greeter = new GreeterClient(channel);
var result = await greeter.SayHelloAsync(new HelloRequest { Name = "Alice" });When creating a YetAnotherHttpHandler instance, you can configure the following HTTP client settings.
Once the handler sends a request, these settings become immutable and cannot be changed.
| Property | Description |
|---|---|
| PoolIdleTimeout | Gets or sets an optional timeout for idle sockets being kept-alive. Default is 90 seconds. |
| MaxIdlePerHost | Gets or sets the maximum idle connection per host allowed in the pool. Default is usize::MAX (no limit). |
| Http2Only | Gets or sets a value that indicates whether to force the use of HTTP/2. |
| SkipCertificateVerification | Gets or sets a value that indicates whether to skip certificate verification. |
| OnVerifyServerCertificate | Gets or sets a custom handler that validates server certificates. |
| OnResolveDns | Gets or sets the name resolver. Defaults to SystemDnsResolver.Resolve (managed System.Net.Dns); set to null to resolve in the native runtime. See Customizing DNS resolution. |
| DnsCacheFallbackDuration | Gets or sets how long a previously resolved address stays usable after a name lookup fails. Default is 24 hours; set to TimeSpan.Zero to disable. |
| RootCertificates | Gets or sets a custom root CA. By default, the built-in root CA (Mozilla's root certificates) is used. See also https://github.com/rustls/webpki-roots. |
| OverrideServerName | Gets or sets a value that specifies subject alternative name (SAN) of the certificate. |
| ClientAuthCertificates | Gets or sets a custom client auth key. |
| ClientAuthKey | Gets or sets a custom client auth certificates. |
| Http2InitialStreamWindowSize | Gets or sets the SETTINGS_INITIAL_WINDOW_SIZE option for HTTP2 stream-level flow control. |
| Http2InitialConnectionWindowSize | Gets or sets the max connection-level flow control for HTTP2 |
| Http2AdaptiveWindow | Gets or sets whether to use an adaptive flow control. Enabling this will override the limits set in http2_initial_stream_window_size and http2_initial_connection_window_size. |
| Http2MaxFrameSize | Gets or sets the maximum frame size to use for HTTP2. |
| Http2MaxHeaderListSize | Gets or sets the max size of received header frames for HTTP2. Default is currently 16KB, but can change. |
| ConnectTimeout | Gets or sets timeout for TCP connection establishment. Pass null to never timeout. Default is never timeout. |
| Http2KeepAliveInterval | Gets or sets an interval for HTTP2 Ping frames should be sent to keep a connection alive. Pass null to disable HTTP2 keep-alive. Default is currently disabled. |
| Http2KeepAliveTimeout | Gets or sets a timeout for receiving an acknowledgement of the keep-alive ping. If the ping is not acknowledged within the timeout, the connection will be closed. Does nothing if http2_keep_alive_interval is disabled. Default is 20 seconds. |
| Http2KeepAliveWhileIdle | Gets or sets whether HTTP2 keep-alive should apply while the connection is idle. If disabled, keep-alive pings are only sent while there are open request/responses streams. If enabled, pings are also sent when no streams are active. Does nothing if http2_keep_alive_interval is disabled. Default is false. |
| Http2MaxConcurrentResetStreams | Gets or sets the maximum number of HTTP2 concurrent locally reset streams. See the documentation of h2::client::Builder::max_concurrent_reset_streams for more details. The default value is determined by the h2 crate. |
| Http2MaxSendBufferSize | Gets or sets the maximum write buffer size for each HTTP/2 stream. Default is currently 1MB, but may change. |
| Http2InitialMaxSendStreams | Gets or sets the initial maximum of locally initiated (send) streams. This value will be overwritten by the value included in the initial SETTINGS frame received from the peer as part of a connection preface. |
| UnixDomainSocketPath | Gets or sets the path to a Unix Domain Socket to be used as HTTP communication channel instead of the default TCP. |
| ResponsePipeOptions | Gets or sets the options for the pipe used to receive the response body. |
Most of them expose hyper client settings, so please check those as well.
gRPC requires communication over HTTP/2, but when it's a non-HTTPS connection, it defaults to attempting a connection with HTTP/1, causing connection issues.
In this case, setting the YetAnotherHttpHandle.Http2Only property to true allows for connections via HTTP/2 over cleartext (h2c).
using var handler = new YetAnotherHttpHandler() { Http2Only = true };Currently, YetAnotherHttpHandler uses Mozilla's root certificates derived from webpki as the root CA.
If you want to use a self-signed certificate or a certificate issued by your organization, you need to set a custom root CA. In this case, you can specify the root certificates in pem format to RootCertificates property.
var rootCerts = @"
-----BEGIN CERTIFICATE-----
MIIE9TCCAt2gAwIBAgIUUQ33LbUPwlgKXmzA77KmDbV2uYkwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDcyNTAzNDYzNFoXDTMzMDcy
MjAzNDYzNFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEAyuyNn36Sv87u8q7UmB7nhuMe71w6geUstcYKhO5ZahYf
d9I9mGZTKpUvThgm65nrIPT8zE7yRqrgagP+MtuRtwByt9w7lO8Y/lJda4iHaTXd
e9Yq0lZGrv0CeZ7NJZCGfPG9GJHG8Bh4IjjhMwGcNea50vfky72nuZnCdLKLbr55
037bIQ7R2bPfxqNTo0Lcij5ApI6/YlpJZ14vi0yHDSCyTAM9PUlgv6EsYdQ3vf1C
bdg2VlnPiAyYI2f7TRZ3YBrrUU8/qcBSsPoTNYgCaBld35/3JizLZJlWukPWnbe3
TuU9FwRv/Vh+UnD2cnv7p0+JW2coa/9Yrk/W7oSFxGoujg/fKm7O9j76JKD/04U7
yGkizQG4uako3BTcIDgHRsDqyIp9MR2v/nbb8Xol2cHL9nE3+ovrgn9upIFvgZk+
nAuRgAmB4IaBtMS5ih0QJnlLB5FqDj+PkJG+s8iqOphg4V3P07zAvOTk1J96VDLO
lnQHpjwMGXoYaevWHRU+Vmm2rktpTyJVt5xtlqjoN/FBnCYbQpAosS5fciN7ghcs
zCmKVKC0riCa7MwPUooVOa/TqDzv5rGPp2vFXTdKDova7OlTo2YofDd2grOwM5O7
TQp7MHUs1gtnHSEYdMeKWi6fSbtx4Jru13blXV7MMUHaQCg2YpJIqofnXQ5+9FMC
AwEAAaM/MD0wCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwIwYDVR0RBBwwGoINd3d3
LmxvY2FsaG9zdIIJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4ICAQAByseWC2Fp
K8S9c/6aHQRwJPgkq1CGMx20wid1XvD9e+F8xF//L5MPa21gx6TC/dRO3rC6Oyqt
mR011vMIh0/DMOhF8eFg9bn8dGSkUiWanw8OKsewTGzJFkoq4cDgO0hjfQTLRNnf
KlDMZLISsnPFSQnhRN7c71j0dXrG+flsazK4CFy9FtXJEePkiCyS5ZSBDkfBKerp
fif6Wz2Zk4yLwmmw0c/sNsgHkRfj3q+Zf1RgpcuUYmYbPigHSI2qpsWbqMeQmIvS
+s7Tap3sQFCYIGCvSmOV4STY2JqxeWOGgR/xLZBpfJgdllfy1mpe7dHpi0kVTEdE
cC1pNeFDn8xYm2M61oGUYy+b035HqD9SfPsnHOFzwgwINuHdL4xjmT+HwAtW+WOj
105d+aIK55gOaJPGa7Pc7UMYtN7Oc/9hWfYti0MXnsyYfCNx6Fl7jtKs1AG2BbQd
sReZj7em23DBe75I4+DCcNWQg40HXsDo2h+z+Xk3SFb/gvHMtmzFudKCDIpD0PS5
gEXEzkKRg/++6iXGF16eBibZ8PED6416rGJz1Bo1YpXSyYCZG6oWwXZTg9fvDZX5
FfLnQACV02y2Gs74h23Yq+QmA30Ly5GPrR5SBRaNiCY1SS7gCAfRah9zJjvbXNGw
h0Eq48Dlw12GaUww3y05/IoAJxtHxZigdQ==
-----END CERTIFICATE-----
";
using var handler = new YetAnotherHttpHandler() { RootCertificates = rootCerts };We strongly not recommend this, but in some cases, you may want to skip certificate validation when connecting via HTTPS. In this scenario, you can ignore certificate errors by setting the SkipCertificateVerification property to true.
You can customize the server certificate verification process by setting the OnVerifyServerCertificate property.
The callback should return true or false based on the verification result. If the property is set, the root CA verification is not performed.
using var httpHandler = new YetAnotherHttpHandler()
{
OnVerifyServerCertificate = (serverName, certificate, now) =>
{
var cert = new X509Certificate2(certificate);
return serverName == "api.example.com" &&
cert.Subject == "CN=api.example.com";
}
};Host names are resolved in managed code by default: OnResolveDns starts out set to SystemDnsResolver.Resolve, which wraps System.Net.Dns.GetHostAddresses. Assign your own handler to replace it:
using var httpHandler = new YetAnotherHttpHandler()
{
OnResolveDns = host => MyResolver.Resolve(host),
};Or compose with the default when you only need to special-case some hosts:
OnResolveDns = host => ResolveSpecialCase(host) ?? SystemDnsResolver.Resolve(host),Setting the property to null hands resolution back to the native runtime (hyper's GaiResolver), which is what upstream YetAnotherHttpHandler does:
OnResolveDns = null,Both paths ultimately reach the platform resolver, so the default is not by itself a behavioural change. What it buys is a single place to observe, wrap or replace resolution — including on platforms where the correct resolver is only reachable from managed code, as on Android below.
Notes:
- The handler runs on a background thread and is allowed to block, so it can call a synchronous platform API directly.
- Return
nullor an empty array to report failure; throwing is also treated as failure. Either way the cache fallback is consulted before the request fails. - At most 32 addresses are used, and the connector tries them in the order given.
- The host name arrives lower-cased and without a scheme or port, because hyper normalises the authority before resolving.
- The resolver is not called at all when the URI already contains an IP literal — there is nothing to resolve, so the connector skips it.
This is automatic — no configuration needed. On Android, SystemDnsResolver.Resolve resolves through the currently active Network before falling back to System.Net.Dns.
getaddrinfo — which both System.Net.Dns and hyper's own resolver reach — resolves against whichever network the process is bound to. If an Android app is backgrounded and that network goes away, every later lookup fails with EAI_NODATA ("No address associated with hostname") until the process re-binds, so the app returns to the foreground and every request fails. Android's own bindProcessToNetwork documentation states this outright and recommends per-network resolution instead.
The implementation is in SystemDnsResolver.cs, guarded by #if UNITY_ANDROID && !UNITY_EDITOR, and does roughly this:
// Never cached: a stale Network handle is exactly what this avoids.
using var activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer")
.GetStatic<AndroidJavaObject>("currentActivity");
using var connectivity = activity.Call<AndroidJavaObject>("getSystemService", "connectivity");
using var network = connectivity.Call<AndroidJavaObject>("getActiveNetwork");
if (network == null) return null; // No connectivity: fall through to System.Net.Dns.
var inetAddresses = network.Call<AndroidJavaObject[]>("getAllByName", host); // InetAddress[]
foreach (var inetAddress in inetAddresses)
{
addresses.Add(new IPAddress(inetAddress.Call<byte[]>("getAddress")));
inetAddress.Dispose();
}Points worth knowing if you replace or extend it:
- The whole call is bracketed by
AndroidJNI.AttachCurrentThread()/DetachCurrentThread(). The resolver runs on a native runtime thread that Unity never attached, so JNI is illegal without it — and detaching matters just as much, because the runtime retires idle threads and would otherwise leak a JVM thread reference per retired thread. getActiveNetwork()requires API 23. Older devices throw and fall through toSystem.Net.Dns.getAllByNamerequiresandroid.permission.INTERNETand must not run on the UI thread. It doesn't — the resolver is always called on a background thread.- Every failure, including an unknown host, falls through to
System.Net.Dnsand then to the cache fallback. - The Editor is excluded (
!UNITY_EDITOR), since there is no AndroidConnectivityManagerwhen playing in the Editor with Android as the target platform.
Successful lookups are remembered per host, and are used only when a later lookup fails. This is controlled by DnsCacheFallbackDuration, which defaults to 24 hours:
using var httpHandler = new YetAnotherHttpHandler()
{
DnsCacheFallbackDuration = TimeSpan.FromMinutes(30), // or TimeSpan.Zero to disable
};The fallback applies to whichever resolver is in use — the managed default, a custom OnResolveDns, or the native one. Because it only ever engages after a lookup has already failed, the trade-off is not "stale address vs. fresh address" but "stale address vs. a failed request" — an address that has genuinely gone away simply fails to connect, leaving the caller no worse off. Shorten or disable it if your services rely on DNS-based failover and you would rather fail fast than reach a decommissioned address.
Unix Domain Sockets can be used as the HTTP transport layer for local usecases (e.g. IPC based on gRPC), instead of network-based TCP.
Set the UnixDomainSocketPath property to enable UDS-based communication to a server listening at the given path:
using var handler = new YetAnotherHttpHandler() { Http2Only = true, UnixDomainSocketPath = "/tmp/example.sock" };
using var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpHandler = handler });Note:
- HTTPS is not supported over UDS. All HTTPS related configuration properties are ignored if UDS is enabled.
- The grpc-dotnet library doesn't handle non-HTTP schemes (like "unix://"), so keep passing an HTTP URI to
GrpcChannel, e.g. http://localhost. The actual HTTP requests will be redirected toUnixDomainSocketPathby YetAnotherHttpHandler internally. - When using Kestrel on the server, you need to set the
KestrelServerOptions.AllowAlternateSchemesoption totrue.
YetAnotherHttpHandler's communication processing is run on worker threads of the tokio runtime. The number of worker threads is determined based on the number of CPU cores by default.
You can specify a number of worker threads using the YetAnotherHttpHandler.SetWorkerThreads method.
When attempting to execute a request (yaha_init_runtime), the following exception message may be displayed:
System.DllNotFoundException: Unable to load DLL 'Cysharp.Net.Http.YetAnotherHttpHandler.Native' or one of its dependencies: The specified module could not be found. (0x8007007E)
This may be because the Visual C++ Redistributable Package (vcruntime140.dll) is not installed. Please download and install it from Microsoft's website.
To debug run in your local environment, you first need to build the native library. Please specify the target as follows when building.
cargo build --target x86_64-pc-windows-msvcWhen debugging or running unit tests, the native library is loaded from the following directory.
- native/targets/{arch}-{os}-{toolchain}/{debug,release}/{lib}yaha_native.{dll,so}
When creating a package, The following artifacts directory is used.
- native/artifacts/{.NET RID}/{lib}yaha_native.{dll,so}
# Generate THIRD-PARTY-NOTICES using cargo-about
cargo about generate about.hbs > ../THIRD-PARTY-NOTICESMIT License
This library depends on third-party components. Please refer to THIRD-PARTY-NOTICES for their licenses.