調査方法 ↓のようなJavaScriptを用意しました。
JavaScript const xhr = new XMLHttpRequest();
xhr.timeout = 5000; // タイムアウト5000ミリ秒
xhr.addEventListener('readystatechange', (e) => {
console.log('[readystatechange]', xhr.readyState, (() => {
if (xhr.readyState === 0) return '(UNSENT)';
if (xhr.readyState === 1) return '(OPENED)';
if (xhr.readyState === 2) return '(HEADERS_RECEIVED)';
if (xhr.readyState === 3) return '(LOADING)';
if (xhr.readyState === 4) return '(DONE)';
})());
});
xhr.addEventListener('loadstart', (e) => {
console.log('[loadstart]');
});
xhr.addEventListener('progress', (e) => {
console.log('[progress]', `${e.loaded} / ${e.total} bytes`);
});
xhr.addEventListener('abort', (e) => {
console.log('[abort]');
});
xhr.addEventListener('error', (e) => {
console.log('[error]');
});
xhr.addEventListener('load', (e) => {
console.log('[load]', `xhr.status = ${xhr.status}, xhr.responseText = ${xhr.responseText}`);
});
xhr.addEventListener('timeout', (e) => {
console.log('[timeout]');
});
xhr.addEventListener('loadend', (e) => {
console.log('[loadend]');
});
xhr.open('GET', 'http://localhost/api.php');
xhr.send();
これは XMLHttpRequest
で http://localhost/api.php
をGETし、XMLHttpRequest
の全イベント をハンドリングしてコンソールにメッセージを出力するようになってます。タイムアウトは5000ミリ秒を設定してます。
上記のスクリプトを http://localhost/api.php
とは別のサーバー (http://localhost:3000
) 上のHTMLから実行して、結果を調べていきます。
なお、api.php
の中身は↓のような雰囲気です。この記事では、これを書き換えることでレスポンスを操作していきます。
api.php (ステータス200) <?php echo '<?php'; ?>
header('Access-Control-Allow-Origin: *');
header('Content-Type: text/plain');
echo 'レスポンス';
※JavaScriptを実行するページ http://localhost:3000
と http://localhost/api.php
のドメインが異なる関係で、CORS制約 回避のため、レスポンスヘッダに Access-Control-Allow-Origin
を設定してます。
ステータスコード200の場合 api.php
がステータスコード200で文字列「ABCDEFGHIJ」をレスポンスするようにして、上記のJSを実行します。
api.php <?php echo '<?php'; ?>
header('Access-Control-Allow-Origin: *');
header('Content-Type: text/plain');
echo 'ABCDEFGHIJ';
JS実行結果 [readystatechange] 1 (OPENED)
[loadstart]
[readystatechange] 2 (HEADERS_RECEIVED)
[readystatechange] 3 (LOADING)
[progress] 10 / 10 bytes
[readystatechange] 4 (DONE)
[load] xhr.status = 200, xhr.responseText = ABCDEFGHIJ
[loadend]
readystatechange (1)
→ loadstart
→ readystatechange (2)
→ readystatechange (3)
→ progress
→ readystatechange (4)
→ load
→ loadend
の順にイベントが発火しました。普通は load
のイベントハンドラでいろいろやるんじゃないでしょうか。
ステータスコード400の場合 レスポンスボディはそのままに、ステータスコードだけ400に変更します。
api.php <?php echo '<?php'; ?>
header('Access-Control-Allow-Origin: *');
header('Content-Type: text/plain');
echo 'ABCDEFGHIJ';
http_response_code(400);
JS実行結果 [readystatechange] 1 (OPENED)
[loadstart]
[readystatechange] 2 (HEADERS_RECEIVED)
[readystatechange] 3 (LOADING)
[progress] 10 / 10 bytes
[readystatechange] 4 (DONE)
[load] xhr.status = 400, xhr.responseText = ABCDEFGHIJ
[loadend]
イベントの発火順はステータスコード200の場合とまったく同じでした。
通信に成功した場合、ステータスコードに関わらず load
が発火します。ステータスコードで処理を分ける場合は xhr.status
を見て判断します。
サーバーが落ちてる場合 サーバーを落として http://localhost/api.php
にアクセスできないようにして、JSを実行してみます。
JS実行結果 [readystatechange] 1 (OPENED)
[loadstart]
[readystatechange] 4 (DONE)
[error]
[loadend]
URLにアクセスできなくなって初めて error
が発火しました。loadend
はエラーの有無に関わらず発火します。
Chromeのバージョンとかによるかもですが、XMLHttpRequest
はサーバーが落ちてると4秒くらいで諦めるようです。
タイムアウトした場合 レスポンスに時間がかかるようにして、XMLHttpRequest
のタイムアウト (ここでは5000ミリ秒 = 5秒) を破ります。
api.php <?php echo '<?php'; ?>
header('Access-Control-Allow-Origin: *');
header('Content-Type: text/plain');
echo 'ABCDEFGHIJ';
http_response_code(200);
sleep(6);
JS実行結果 [readystatechange] 1 (OPENED)
[loadstart]
[readystatechange] 4 (DONE)
[timeout]
[loadend]
timeout
イベントが発火しました。error
は発火しません。loadend
はタイムアウトでも発火してます。
CORS制約でブロックされた場合 せっかくだから、CORS制約でブラウザにレスポンスをブロックされるケースもやってみます。APIが Access-Control-Allow-Origin
ヘッダを返さないように変更します。
api.php <?php echo '<?php'; ?>
header('Content-Type: text/plain');
echo 'ABCDEFGHIJ';
http_response_code(200);
JS実行結果 [readystatechange] 1 (OPENED)
[loadstart]
Access to XMLHttpRequest at 'http://localhost/api.php' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
[readystatechange] 4 (DONE)
[error]
[loadend]
Chromeでやってるので、コンソールに Access to XMLHttpRequest at 'http://localhost/api.php' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
エラーが出力されました。
イベントの発火順は、サーバーが落ちてる場合と同じでした。
xhr.abort() で中断した場合 ちょっと特殊ケースです。api.php
は置いといて、JavaScriptの末尾に xhr.abort()
を追加して実行します。
JavaScript (末尾) // (略)
xhr.open('GET', 'http://localhost/api.php');
xhr.send();
xhr.abort(); // 追加
JS実行結果 [readystatechange] 1 (OPENED)
[loadstart]
[readystatechange] 4 (DONE)
[abort]
[loadend]
abort
イベントが発火しました。この場合も error
は発火せず、loadend
は発火します。
ステータスコード200の場合 (Transfer-Encoding: chunked) progress
イベントが複数回発生するところが見たかったので、Transfer-Encoding: chunked
を使って、5000バイトずつ2回に分けてレスポンスするよう api.php
を変更します。
api.php <?php echo '<?php'; ?>
header('Access-Control-Allow-Origin: *');
header('Content-Type: text/plain');
header('Transfer-encoding: chunked');
$chunk1 = str_repeat('!', 5000);
echo dechex(strlen($chunk1))."\r\n".$chunk1."\r\n";
flush();
sleep(1);
$chunk2 = str_repeat('?', 5000);
echo dechex(strlen($chunk2))."\r\n".$chunk2."\r\n";
flush();
sleep(1);
echo '0'."\r\n\r\n";
JS実行結果 [readystatechange] 1 (OPENED)
[loadstart]
[readystatechange] 2 (HEADERS_RECEIVED)
[readystatechange] 3 (LOADING)
[progress] 5000 / 0 bytes
[readystatechange] 3 (LOADING)
[progress] 10000 / 0 bytes
[readystatechange] 4 (DONE)
[load] xhr.status = 200, xhr.responseText = !!!!!!!!!!!!!!!!!!!!!!!!!!!!(※注:長いので略)
[loadend]
progress
だけでなく readystatechange (3)
も2回発火しました。
なお、ここでは Content-Length
ヘッダをレスポンスしてないので、progress
イベントの e.total
がゼロになっています。RFC2068では Transfer-Encoding: chunked
のとき Content-Length
は返してはいけないそうです。本来は全体長がわからないものを返すために使うからだと思います。
Transfer-Encoding: chunked について - 理系学生日記
タイムアウトした場合 (Transfer-Encoding: chunked) Transfer-Encoding: chunked
の途中でタイムアウトさせてみます。トレイラーを返さなければできます。
api.php <?php echo '<?php'; ?>
header('Access-Control-Allow-Origin: *');
header('Content-Type: text/plain');
header('Transfer-encoding: chunked');
$chunk1 = str_repeat('!', 5000);
echo dechex(strlen($chunk1))."\r\n".$chunk1."\r\n";
flush();
JS実行結果 [readystatechange] 1 (OPENED)
[loadstart]
[readystatechange] 2 (HEADERS_RECEIVED)
[readystatechange] 3 (LOADING)
[progress] 5000 / 0 bytes
[readystatechange] 4 (DONE)
[timeout]
[loadend]
progress
が発火した後レスポンスを待ってましたが、タイムアウトした段階で readystatechange (4)
→ timeout
の流れになりました。普通のタイムアウトと同じですね。
以上 他に何か思いついたら追加で調べるかもしれません。