一個公司內部供iPhone使用的網站,這幾天陸續接獲User回報: 更新iOS6後再連上網站程式壞光光!! 依User的描述,系統整個亂七八糟,完全無法使用。想繼續探究詳情,User也說不清楚,讓人格外好奇: iOS 6究竟做了什麼修正,竟可以讓原本正常的網站瞬間崩壞?

直接借到iOS 6手機後,才知道User所謂的系統亂七八糟所謂何來? 網站大量使用了AJAX方式查詢及更新資料,而由觀察所得,發現iOS 6的瀏覽器有很強的Cache行為,即便我們使用了GET時加上亂數QueryString參數的技巧規避Cache,照樣會讀到更新前的結果。

而經過比對Server端的資料奱化,發現更驚人的事實 -- 部分$.post()動作並沒有真的更新到Server端!! 由此現象,初步獲得"iOS 6 Safari連POST Request都會Cache住"的推論。在Stackoverflow網站上,看到一則熱門討論,有相似的結論:

I suspect that Apple is taking advantage of this from the HTTP spec in section 9.5 about POST:

Responses to this method are not cacheable, unless the response includes appropriate Cache-Control or Expires header fields. However, the 303 (See Other) response can be used to direct the user agent to retrieve a cacheable resource.

So in theory you can cache POST responses...who knew. But no other browser maker has ever thought it would be a good idea until now. But that does NOT account for the caching when no Cache-Control or Expires headers are set, only when there are some set. So it must be a bug.

依HTTP規格,的確沒規定POST不能Cache,但顯然iOS 6的Safari想獨步全球,引領風潮,成為全世界第一個會Cache POST的瀏覽器!!

這個行為改變,不用多說,肯定會讓一堆現有網頁程式失常(沒人想過POST Response會引用自Cache),搞得網頁開發圈天下大亂。試想,當你POST送出網頁表單,只要該表單內容先前曾傳送過,第二次傳送時Safari根本不會送出表單,而是直接拿上回傳送的結果回給User,讓User誤以為資料完成了第二次更新... 好一個令人充滿**WTF**感受的創舉~

經過實測,我找出同時加入以下兩種做法避開POST被Cache的解法:

  1. 設定Cache-Control: no-cache(註: 設定max-age=0, Expires無效),在ASP.NET可使用Response.Cache.SetCacheability(HttpCacheability.NoCache);實現。
    (由於Expires無效只認no-cache,有人認為這未必是iOS 6 Safari的設計原意,而是Bug,倒也頗有道理)
  2. 在我的實測中,光設定no-cache還不夠,還需要在POST內容中加入_rand=Math.random()或_timeStamp=new Date().getTime()之類每次不同的亂數參數,讓送出內容有所差異,才能確保不被Cache所誤。

我的案例是用ASP.NET MVC,為了簡化在每個Action加上設定NoCache的工夫,決定善用ASP.NET MVC方便的ActionFilter功能:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace MobileMvc.Models
{
    public class NoCacheAttribute : ActionFilterAttribute
    {
        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            filterContext.HttpContext.Response.Cache.SetCacheability(
                                                        HttpCacheability.NoCache);
            base.OnResultExecuted(filterContext);
        }
    }
}

如此,對於需要指定不得Cache的動作,簡單加註Filter即可:

[NoCache]
public ActionResult PostSomething()
{
    …程式邏輯...
}

雖然已找到替代解決方案,但按照慣例: 補聲"暗"!

【延伸閱讀】


Comments

# by 小黑

黑大,好強大,太棒了

# by jain

這也太新的想法吧?

# by 新手阿彥

請問大大: 您的第2點解法,在POST內容加上亂數參數 在非MVC的寫法要如何實作?

# by Jeffrey

to 新手阿彥, 有個簡單做法是在<form>中放入一個<input type="hidden" name="_rand" id="_rand" />,接著由Server端或Client端放入亂數值都可,前端可以用$(function() { $("_rand").val(Math.random()); }的jQuery寫法輕鬆實現。

# by 新手阿彥

感謝黑大 ^^

# by lynn

如果Cache-Control: no-cache 用 no-store 替換,是不是就一勞永逸了。

Post a comment