BLOG ブログ

今年の難題MVP賞「2度目あるPOSTはFINされる」

クリスマスが終わって一息ついたら、もう街中の飲み屋街では一本締めの手拍子が聞こえ始めてまいりました。それを聞いていよいよ年の瀬だなと感じている、インフラ担当の北瀬です。

今年を振り返って最も苦労して解決した難題案件を紹介します。
それは・・・

jQuery/ajaxを利用してWebサーバーと通信すると2回目のPOSTに失敗するケース・環境がある

何が難題かというよ、同じOS・ブラウザーバージョンまできっちり合わせても再現性が無く、まさに怪現象といいますか、

な… 何を言っているのか わからねーと思うが
おれも 何をされたのか わからなかった…
』(某漫画

状態で、開発チーム全員で頭を抱える自体になりました。

遭遇が極めてレアケースで情報が少ないので、解決までの道のりを他のエンジニアのために纏めてたいと思います。

序章

お客様からお問合せがあり、所有するiPhoneのSafariでもChromeでもページを進めず「ページを開けません。ネットワーク接続が切れました。」となるとのこと。

メッセージ通りに受け取るなら(新幹線などで)移動中に電波切れ、WiFiの切り替わりが想定されますが、、。

  • どうも特定端末では、何回やり直しても100%再現し、そのような端末が複数台ある。
  • しかし、多数のiPhoneやパソコン端末では発生しない。

というものでした。

当初はスマホ(iPhone)だけの問題かと思われたのですが、最終的に全く違う事になるとはこの時は知る由も無く…。

先が見えない

スマホのバージョンや、ブラウザ、起きている状況などを色々お聞きしつつ、弊社で色々なデバイス、バージョン、OSなどを組み合わせて検証を重ねるもどうしても再現できませんでした。

動作不具合であればこの段階で原因が分かるか手がかりが見つかる事が大半ですが、見つからず。

スマホでも発生有無が分かれていたので開発チームで再現するか五分五分の中でしたが、お客様がお使いの本番環境へのアクセスをお願いして、環境を見させていただきました。

徐々に明らかになる

有り難い事に、開発チームで所有する端末で本番環境へのアクセスで再現しました。
さらに調査を進めると、スマホ以外にmacやWindowsでも発生する事が分かりました。

  • iPhone/macOS Safari
    「ページを開けません。ネットワーク接続が切れました。」
  • Windows Internet Explorer 11 (IE11) のコンソールログ
    「XMLHttpRequest: ネットワーク エラー 0x2ef3, エラー 00002ef3 のため操作を完了できませんでした。」
  • Webサーバー(IIS)
    同エラーが発生した時のアクセスログは記録されない。

しかし・・・

具体的なエラーメッセージが拾えたIE11の事例を探すも、日本語のサイトはおろか英語圏・他の言語のサイトでも皆無に近い状態で情報がなく、トライ&エラーで時間を掛けての確認が必要でした。

再現コード

最もシンプルなパターンが分かりました。
※指定しているURLは何回POSTしても正常(HTTP 200 OK)応答とします。

var formData = new FormData(document.getElementById('#input-form'));

jQuery.ajax({
  url: 'http://example.com/~',
  type: 'POST',
  data: formData
}).done(function(response) {
  // 成功したときの処理
}).fail(function(function(jqXHR, textStatus, errorThrown) {
  // 失敗したときの処理
});

はい。何も特別な処理はしていない、ありふれたサンプルです。

しかし、これを1回目のPOSTが成功するものの、ページ遷移しないでもう1回呼び出すと失敗する。さらに次は成功する。また次は失敗…。これの繰り返しです。

失敗した時のfailコールバックは以下の状態となりました。

jqXHR.status – 0
jqXHR.statusText – “error”
jqXHR.readyState – 0
errorThrown – “”

Webサーバーからの応答が入るはずのstatusが0で、かつreadyStateが0なので、POSTデータ準備中に何か問題が起きたと考えられていたのですが、、、

原因

Wiresharkによるパケットキャプチャしてようやく原因がわかりました。

POST応答時に「Connection」ヘッダーが無い場合、つまりHTTP/1.1の既定の動作としてキープアライブが有効になった(Connection: keep-aliveヘッダー相当)通信で起きていました。

1回目にPOST送信・応答が完了すると、その送信に利用したHTTP接続(TCPセッション)がキープアライブで一定時間保持したままになります。

そのキープアライブ期間中に2回目のPOST送信をすると、送信直後に急にWebサーバー側から「FIN」パケットが飛んでくる。
ブラウザーからもTCPプロトコルの規約に則って「FIN」を返すも、さらに「RST」パケットが飛んでくる。

いやいや、POST応答パケットまだ貰ってないんだけど…。という状態です。

補足:FINは誰が?

この原因で注目すべきは「FIN」は誰が返したのか。という点です。

社内の検証環境はWebサーバーと試験用端末は「同じネットワーク:LAN」内で再現しませんでしたし、
その他多くのユーザー様やクラウドなど色々なシステム構成で稼働していますが、同様のケースはありませんでした。

Webサーバー自身(HTTP.sysカーネル?)が返したのか、ルーターやファイアウォール等のネットワーク機器が返したのか、、実のところ、これは謎のままです。

再現性の違い

再現するスマホやmac/Windows端末では、FINを受け取った時点でXMLHttpRequest.send()が例外エラーを発します。これをラッピングしているjQuery.ajax()が例外を受け取ってfailコールバックするというステップを踏みます。

再現しない端末では、自動的にTCPセッションを3-wayハンドシェイクから確立しなおして、2回目のPOST要求を再送してPOST応答を受け取っていました。

対処

POST要求・応答時はキープアライブしないというのが根本的な対処になります。

現象の切り分けとしては、IISの「共通 HTTP応答ヘッダーの設定」ダイアログにある「HTTP Keep-Alive を有効にする」のチェックを外して無効化してみます。
これでWebサーバーからの応答に「Connection: close」ヘッダーが付加されるようになります。

副作用

ただし、画像等のリソース応答に対しても全てにおいて無効になるので、接続・応答パフォーマンスが極端に落ちるリスクがあります。
運用環境で試す前に慎重な検討が必要です。

ようやく解決

IISの設定を変更することで、お問合せ元のお客様は解決に至りました。
最終的に2カ月弱かかった案件となりました。

あとがき

色々なブログや、英語圏のコミュニティサイトを方々探し回っても同様の現象に遭遇した人は非常に少なく、対処療法的なアドバイスが散見されるだけで、やってみた人も効果があったり、無かったりと報告もまちまちで、「正しい判断を見極める=ミスリードで大多数のお客様に迷惑を掛けてはならない」根拠探しに多くの時間を割きました。

そもそも手元の検証環境で問題を再現できない状況だったので、各所で見かける方法が効果的か否かの判断すらも、机上で判断するしかないという状況が続きました。。

再発防止のため主原因と発生条件を特定し、根拠ある有効な対策を確立するまでの道のりが今年一番長かった案件でした。

それでは、良いお年を。

参考文献