【Unity】UnityWebRequestの詳細なエラー内容で処理を分岐する方法

https://twitter.com/su10_dev/status/1678415182106591233?s=20

# はじめに
UnityでHTTP(S)通信したいなーと思ったら通常は組み込みの `UnityWebRequest` を使うと思うのですが、詳細なエラー内容で分岐できなくて困ったので調べました。

# 大まかなエラーで分岐する
[コガネブログ](https://baba-s.hatenablog.com/entry/2021/01/25/090000)さんの記事内のコードを以下に引用します。

```cs
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class Example : MonoBehaviour
{
    private IEnumerator Start()
    {
        var request = UnityWebRequest.Get( "https://baba-s.hatenablog.com/" );

        yield return request.SendWebRequest();

        switch ( request.result )
        {
            case UnityWebRequest.Result.InProgress:
                Debug.Log( "リクエスト中" );
                break;

            case UnityWebRequest.Result.Success:
                Debug.Log( "リクエスト成功" );
                break;

            case UnityWebRequest.Result.ConnectionError:
                Debug.Log
                (
                    @"サーバとの通信に失敗。
リクエストが接続できなかった、
セキュリティで保護されたチャネルを確立できなかったなど。"
                );
                break;

            case UnityWebRequest.Result.ProtocolError:
                Debug.Log
                (
                    @"サーバがエラー応答を返した。
サーバとの通信には成功したが、
接続プロトコルで定義されているエラーを受け取った。"
                );
                break;

            case UnityWebRequest.Result.DataProcessingError:
                Debug.Log
                (
                    @"データの処理中にエラーが発生。
リクエストはサーバとの通信に成功したが、
受信したデータの処理中にエラーが発生。
データが破損しているか、正しい形式ではないなど。"
                );
                break;

         default: throw new ArgumentOutOfRangeException();
        }
    }
}
```

`UnityWebRequest.Result` という `enum` で分岐できるっぽいのですが、これは以下の５種類しかありません。


| 値 | 説明 |
|:-----------|:------------|
|InProgress|The request hasn't finished yet.|
|Success|The request succeeded.|
|ConnectionError|Failed to communicate with the server. For example, the request couldn't connect or it could not establish a secure channel.|
|ProtocolError|The server returned an error response. The request succeeded in communicating with the server, but received an error as defined by the connection protocol.|
|DataProcessingError|Error processing data. The request succeeded in communicating with the server, but encountered an error when processing the received data. For example, the data was corrupted or not in the correct format.|

（https://docs.unity3d.com/2020.2/Documentation/ScriptReference/Networking.UnityWebRequest.Result.html）

`成功` or `失敗` くらいの粒度で知りたい場合は別にいいのですが、例えば

* 通信に失敗したときにエラーコードを表示してそれをもとに問い合わせてもらう
* タイムアウトしたら再度リクエストを送信する

みたいな場合には情報が無に等しく、途方に暮れてしまいます。

# 詳細なエラーで分岐する
より詳細なエラーを知りたい場合は `UnityWebRequest.error` が使えます。

```cs
switch (unityWebRequest.error)
{
    case "Cannot resolve destination host":
        DoSomething();
        break;

    ...

    default: throw new ArgumentOutOfRangeException();
}

```

この方法には大きな問題点があって、 **`error` の文字列が公開・一覧化されていない**ということです。

上のコードで言うところの `"Cannot resolve destination host"` みたいな文字列が取りうる値がわからなければ分岐しようがありません。

こんなことってある？？？

# エラーの文字列を知る
とりあえずソースコードを読めばなんとかなるでしょ、ということでUnityCsReferenceを確認してみます。

[UnityWebRequest.bindings.cs](https://github.com/Unity-Technologies/UnityCsReference/blob/906c6894a0014ada369cefde96b51e30acc4246e/Modules/UnityWebRequest/Public/UnityWebRequest.bindings.cs)

```cs
internal enum UnityWebRequestError
```

```cs
private extern static string GetWebErrorString(UnityWebRequestError err);
```

エラーの文字列が決め打ちしてあるわけではないですが、怪しいやつが２つ見つかりました。

リフレクションを使って `GetWebErrorString(UnityWebRequestError err)` を呼んでみます。

```GetAllErrorStrings.cs
using System;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.Networking;

public static class GetAllErrorStrings
{
    [RuntimeInitializeOnLoadMethod]
    static void RuntimeInitializeOnLoadMethod()
    {
        var errorCodes = Enum.GetValues(typeof(UnityWebRequest.UnityWebRequestError))
            .Cast<UnityWebRequest.UnityWebRequestError>()
            .ToList();

        var method = typeof(UnityWebRequest).GetMethod("GetWebErrorString", BindingFlags.Static | BindingFlags.NonPublic);
        var str = "";

        foreach (var errorCode in errorCodes)
        {
            var error = method.Invoke(null, new object[] { errorCode });
            str += $"{errorCode}:{error}\n";
        }
        
        Debug.Log(str);
    }
}
```

`internal` な `UnityWebRequestError` にアクセスしていてそのままではコンパイルエラーになります。[こちら](https://qiita.com/su10/items/7773715c8e43058e533c)を参考に `UnityEngine.UI` などを参照する `*.asmref` を追加してください。

このスクリプトを以下の環境で実行した結果は次の表の通りになりました。

* 実行環境
    * OS：macOS Sonoma 14.3
    * Unity：2022.3.18f1
    * Unityエディタ上で実行

| errorCode | error |
|:----------|:------|
|OK||
|OKCached||
|Unknown|Unknown Error|
|SDKError|Backend Initialization Error|
|UnsupportedProtocol|Unsupported Protocol|
|MalformattedUrl|Malformed URL|
|CannotResolveProxy|Cannot resolve proxy|
|CannotResolveHost|Cannot resolve destination host|
|CannotConnectToHost|Cannot connect to destination host|
|AccessDenied|Access denied|
|GenericHttpError|Generic/unknown HTTP error|
|WriteError|Unable to write data|
|ReadError|Unable to read data|
|OutOfMemory|Out of memory|
|Timeout|Request timeout|
|HTTPPostError|Error during HTTP POST transmission|
|SSLCannotConnect|Unable to complete SSL connection|
|Aborted|Request aborted|
|TooManyRedirects|Redirect limit exceeded|
|ReceivedNoData|Received no data in response|
|SSLNotSupported|Destination host does not support SSL|
|FailedToSendData|Failed to transmit data|
|FailedToReceiveData|Failed to receive data|
|SSLCertificateError|Destination host has an erroneous SSL certificate|
|SSLCipherNotAvailable|Unable to load SSL Cipher for verification|
|SSLCACertError|SSL CA certificate error|
|UnrecognizedContentEncoding|Unrecognized content-encoding|
|LoginFailed|Login failed|
|SSLShutdownFailed|SSL shutdown failed|
|RedirectLimitInvalid|Redirect limit is invalid|
|InvalidRedirect|Encountered invalid redirect (missing Location header?)|
|CannotModifyRequest|Cannot modify request at this time|
|HeaderNameContainsInvalidCharacters|Header name contains invalid characters|
|HeaderValueContainsInvalidCharacters|Header value contains invalid characters|
|CannotOverrideSystemHeaders|Cannot override system-specified headers|
|AlreadySent|Request already transmitted|
|InvalidMethod|Invalid HTTP Method|
|NotImplemented|Not implemented|
|NoInternetConnection|No Internet Connection|
|DataProcessingError|Data Processing Error, see Download Handler error|
|InsecureConnectionNotAllowed|Insecure connection not allowed|

基本的には表の右側のテキストを使って分岐すればいいはずですが、iOSやAndroidの実機では異なる値になるかもしれないので、実機でも確認したほうが良さそうです（確認できたら追記します）。

また、これらのエラー文字列はUnityのバージョンによっても異なる場合があるので、自分が使っているバージョンのUnityで実際に出力してみることをおすすめします。

# 感想
こういうムダな努力をしたくなければ素直に [Best HTTP](https://assetstore.unity.com/packages/tools/network/best-http-267636) を買うのが良さそう。

# いただいたコメント
https://x.com/shiena/status/1761697155176452216?s=20