latest log

酩酊状態で書いたエンジニアポエムです。酩酊状態で読んでください。

Android 2.3 の WebViwe で GET によるクロスドメインリクエストが最初の1回しか成功しない

現象

Unity + WebView 環境において、XHR を使ったクロスドメインリクエストに失敗します。

Async GET なら xhr.readyState = 4 で xhr.status = 0 となり失敗します。
Sync GET なら NETWORK_ERR: XMLHttpRequest Exception 101 でエラーになります。

同様の問題は POST では発生しません。

テストコード

f:id:uupaa:20120820142540p:plain

<!DOCTYPE html><html><head><meta charset="utf-8"><title></title>
<meta name="viewport" content="width=device-width, user-scalable=no">

<script>
var api1 = "http://dev.example.com/api/ping";  // GET
var api3 = "http://dev.example.com/api/login"; // POST


// --- async ajax ---
function async(method, url, data) {
    function _ajax() {
        var xhr = new XMLHttpRequest;

        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    alert(xhr.responseText);
                } else {
                    alert("FAIL: " + xhr.status); // FAIL
                }
            }
        };
        xhr.open(method, url, true);
        if (method === "POST") {
            xhr.setRequestHeader("Content-Type",
                                 "application/x-www-form-urlencoded");
        }
        xhr.send(data ? data : null);
    }
    _ajax();
}

// --- sync ajax ---
function sync(method, url, data) {
    function _ajax() {
        var xhr = new XMLHttpRequest;

        xhr.open(method, url, false);
        if (method === "POST") {
            xhr.setRequestHeader("Content-Type",
                                 "application/x-www-form-urlencoded");
        }
        xhr.send(data ? data : null);
        if (xhr.status === 200) {
            alert(xhr.responseText);
        } else {
            alert("FAIL: " + xhr.status); // FAIL
        }
    }
    _ajax();
}
</script>

</head><body>
<button onclick="async('GET',  api1, null)">call async GET api</button>
<button onclick=" sync('GET',  api1, null)">call sync GET api</button>
<button onclick="async('POST', api3, 'token=')">call async POST api</button>
<button onclick=" sync('POST', api3, 'token=')">call sync POST api</button>

</body></html>

Android 2.3, GET + Sync リクエストで "NETWORK_ERR: XMLHttpRequest Exception 101" が発生(WebViewと標準ブラウザの両方で発生)
f:id:uupaa:20120820141745p:plain

Android 2.3, Get + Async リクエストの2回目で xhr.status = 0 が発生
f:id:uupaa:20120820141752p:plain

発生条件

  1. Android 2.3.4 端末。Android 4.0.4 端末では発生せず
  2. Unity + WebView 環境。Titanium や PhoneGap 環境では未確認
  3. サーバ側でレスポンスヘッダに Access-Control-Allow-Origin: * を返すように設定ずみ


なお、 xhr.withCredentials = true なども試行しましたが効果はありませんでした。

http://caniuse.com/#feat=xhr2 によると、Android 2.3 ブラウザはそもそも XMLHttpRequest Lv2 の CrossDomainRequest をサポートしていないと…

対策

POST は動作するため、GET を JSONP で代用します。
JSONP リクエストを受け付けない場合は、以下のように API を追加し回避します。

現在の API が GET http://dev.example.com/api/hogehoge なら
JSONP用のAPIhttp://dev.example.com/api/hogehoge.json?callback=MyFunctionName のようにします(Twitter APIを参考に)。

URLの拡張子が json で callback=MyFunctionName があったら、戻り値を

function MyFunctionName() {
    return ...;
}

の形で返してもらうようにします。
MyFunctionName の部分は毎回ユニークな名前になります。

条件マトリクス

ブラウザと GET/POST, Async/Sync の各条件で、クロスドメインリクエストが可能かどうかのマトリクスです。

OS Version ブラウザ Async/Sync METHOD クロスドメインリクエスト
Android 2.3.4 標準ブラウザ Async GET
Android 2.3.4 標準ブラウザ Sync GET × *1
Android 2.3.4 標準ブラウザ Async/Sync POST
Android 2.3.4 WebView Async GET × *2
Android 2.3.4 WebView Sync GET × *3
Android 2.3.4 WebView Async/Sync POST
Android 4.0.4 標準ブラウザ Async/Sync GET/POST
Android 4.0.4 WebView Async/Sync GET/POST
iOS 5.1.1 Mobile Safari Async/Sync GET/POST
iOS 5.1.1 WebView Async/Sync GET/POST

Android 2.3.4 - NW-Z1050
Android 4.0.4 - Galaxy Nexus
iOS 5.1.1 - iPhone4S

*1:NETWORK_ERR: XMLHttpRequest Exception 101で常に失敗

*2:初回だけ成功するが、2回目以降のリクエストは常に失敗

*3:NETWORK_ERR: XMLHttpRequest Exception 101で常に失敗