必見の記事

iOS6 Mobile SafariがAjax POSTでキャッシュする問題を回避する方法

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

iOS6出た!

iOS6が出来ましたね。社内ではiPhone5を入手した社員がLTEの速度に驚いていました。私はまだiPhone4Sですが、iOS6にして新しい機能を楽しんでいるところです。さて、iOS6にしてSafariの仕様が変わって困ったと言った投稿が海外のフォーラムで挙っています。それも、基本的なPOST通信についてです。これは困ったということで、動作を確認して対策方法についてシェアしたいと思います。

どんな時にPOSTをキャッシュする?

まずはどんな状況か確認してみたいと思います。

  • Cache-ControlもExpiresも無い場合 : iOS6 Mobile SafariはPOSTをキャッシュする
  • Cache-Control max-age=0指定 と Expires指定 : iOS6 Mobile SafariはPOSTをキャッシュする
  • Cache-Control: no-cache指定 : iOS6 Mobile SafariはPOSTをキャッシュ"しない"

iOS6のリモートデバッガで確認する

iPhoneが実際にPOSTをキャッシュするかiOS6の新機能であるリモートデバッガであるWebインスペクタを有効にします。iPhoneの設定からSafariを選んで詳細から設定できます。ちなみに、実機が無くてもiOSシュミレータを使って同じことができます。

iPhoneとMacを繋げてから、計測したいWebサイトをiPhoneで訪問します。すると、Mac Safariの開発タブにiPhone名が表示されて、訪問したWebサイトのリストから計測したいアドレスを選択できるようになっています。後は、iPhoneでリロードすればOKです。Webインスペクタのウィンドウが立ち上がってネットワーク上のやり取りを確認することができます。

Mac Safari上でiPhone Safariの検証をすることができました!HTTPスタータスコードやキャッシュ有無を確認できましたね。

キャッシュの動作を確認する

実際に問題が起こるか確認してみましょう。おなじみのAmazon EC2を使いまして、node.jsでWebサーバを立てます。これをiPhoneのSafariからアクセスしてMacのSafariでリモートデバッガを使って挙動を確認します。

Cache-ControlもExpiresも無い場合

以下はindex.htmlのソースです。

<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script>
  function formPost(){
    $.ajax( {
      type: "Post",
      url : "/dopost",
      data : { param:"hogehoge" },
      dataType: "json",
      success : function( json ){
        $("#msg").text("success"+json.timestamp);
      },
      error : function (xhr,text,thrown){
        $("#msg").text("error : "+text);
      }
    });
  }
</script>
</head>
<body>
<input type="button" value="POST test" onclick="formPost()">
<p id="msg"></p>
</body>
</html>

以下はnode.jsのソースです。

var http = require('http'),
    sys = require('sys'),
    fs = require('fs');

http.createServer(function (req, res) {
  console.log('accessed');
  if(req.method == 'POST') {
    var body='';
    req.on('data', function (data) {
      body +=data;
    });
    req.on('end',function(){
      console.log(body);
    });
    res.writeHead(200, {'Content-Type':'application/json'});
    res.end('{"timestamp":"'+new Date().toString()+'"}');
  }else{
    fs.readFile('index.html', function(err, content) {
      if (err) { throw err; }
      console.log('loaded');
      res.writeHead(200, {'Content-Type':'text/html; charset=utf-8'});
      res.end(content);
    });
  }
}).listen(80);

process.on('uncaughtException', function (err) {
    console.log('uncaughtException => ' + err);
});

そしてブラウザから動作確認をしてみます。ボタンをクリックするとサーバへAjaxでPOSTを行います。サーバ側では現在の時刻を文字列にして返していますが、何度ボタンを押しても時刻が変わりません。また、サーバ側のログにもアクセスの痕跡がありません。ブラウザがキャッシュを見ていてサーバにアクセスしていないのです。

Cache-Controlにmax-age=0を付けた場合

node.js側で以下のようにヘッダを付与しても、ブラウザ側ではずっとキャッシュしていました。

var dateplus = new Date();
    dateplus.setSeconds(dateplus.getSeconds()+5);
res.writeHead(200, {'Content-Type':'application/json','cache-control':'max-age=5','Expires':dateplus.toString()});

Cache-Controlにno-cacheを付けた場合

node.js側で以下のようにヘッダを付けたところ、ブラウザはPOST結果をキャッシュせずにサーバへリクエストを送っていました。

res.writeHead(200, {'Content-Type':'application/json','cache-control':'no-cache'});

ということで、レスポンスヘッダにCache-Control:no-cacheを指定することで回避できることが分かりました。今回はnode.jsでしたが、Apacheの場合の設定を以下に示したいと思います。POST通信のときのみキャッシュしないヘッダを付与しています。

SetEnvIf Request_Method "POST" IS_POST
Header set Cache-Control "no-cache" env=IS_POST

WebAPI等の利用でサーバ側のヘッダ指定が出来ない場合

外部サービスで提供されているWebAPI等でPOSTを使う場合、サーバ側の設定を変えることができません。この場合の回避策として、POST内容を毎回変えることでキャッシュを回避します。以下のコードを見て下さい。これは、jQueryがajax通信をする前にtimestampをパラメータに付加して送っています。毎回違う値が付加されるので、ブラウザは別物がPOSTされると判断してキャッシュを使いません。この$.ajaxPrefilterブロックの部分は汎用的に使えますので、全てのPOST通信に対応することができます。ちなみに、jqueryのajax引数にはcache:falseを指定することもできますが、これはGET通信時にタイムスタンプを付与するだけで、POST通信時には付与してくれません。

<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script>
  function formPost(){
    $.ajaxPrefilter(function (options, originalOptions, jqXHR) {
      if(originalOptions.type.toLowerCase() == 'post'){
        options.data = jQuery.param($.extend(originalOptions.data||{}, {
          timeStamp: new Date().getTime()
       }));
      }
    });

    $.ajax( {
      type: "post",
      url : "/dopost",
      data : { param:"hogehoge" },
      dataType: "json",
      headers: {
        "pragma": "no-cache"
      },
      success : function( json ){
        $("#msg").append(json.timestamp+'<br>');
      },
      error : function (xhr,text,thrown){
        $("#msg").text("error : "+text);
      }
    });
  }
</script>
</head>
<body>
<input type="button" value="POST test" onclick="formPost()">
<p id="msg"></p>
</body>
</html>

まとめ

今回は、iOS6のMobileSafari環境におけるAjaxのPOST通信のキャッシュ挙動に関する問題を取り上げて、動作確認を行い、回避策を提示しました。今が旬のiOS6でブラウザのバージョンアップによって挙動が変わってしまうと既に動いているアプリケーションへの影響が大変大きいです。今回の問題は、PC SafariやChromeなどでは顕在化しないため、ぱっと見よく分からずに問題の特定に時間が掛かってしまうことです。POSTは普通キャッシュしないでしょ!?という先入観を捨ててサーバやブラウザのデバッガを使って挙動を追う地道な作業が必要です。Appleとしては同じ写真を2回アップロードさせたくない等のユーザの利便性を追っているのかもしれませんが、みんなで仲良く共通ルールを決めるのではなく、天下を取ったヤツがルールだってことでしょうかw。他にもロングポーリングに関する挙動が変わったり、スピナーが終わらなかったりと影響大の変更が行われていますので注意してください。

参考資料

Is Safari on iOS 6 caching $.ajax results?

Understanding the iOS6 AJAX bugs