在前一篇文章“一个伴随ASP.NET从1.0到4.0的OutputCache Bug”中,揭露了ASP.NET OutputCache的一个浏览器缓存的Bug。
在这篇文章中,我们将揭露ASP.NET OutputCache的另一个Bug,这个Bug在去年2月份的时候揭露过一次,但是当时揭露不够彻底,解决方法也不够完美。这里再揭露一次。
背景介绍
为了解决非电信用户访问博客园的网速问题(尤其是北方网通用户),我们准备采用CDN加速。要让CDN加速发挥作用,就要建立有效的页面缓存机制(OutputCache Location要设置为"Any")。为了解决这个实际中的应用问题,必须要好好研究ASP.NET OutputCache,所以才会揭露它的Bug。
重要提示
如果你在ASP.NET应用程序(非ASP.NET MVC)中使用了OutputCache,一定要关注这个Bug,一定要动手去解决它!
问题现象
如果你见过上面的身影,你就曾经与这个Bug相遇过或者重逢过。(不经意的相遇或者重逢,也许只是擦肩而过,也许就是天长地久;不要期待偶然,但也不要忽视偶然。)
当初我们与它相遇时,它“忽隐忽现”(出现在不多见的特定的情况下),让人“神魂颠倒”(面对这个问题无从下手)。
浏览器为什么会给出这个窗口呢?
只是因为Web服务器和浏览器说了一句话(Response Headers):"Content-Type:text/vnd.wap.wml; charset=utf-8"。为什么要说这样的话?请看下面的分解。
问题发生过程
1)当你在一个ASP.NET页面中增加了OutputCache设置,比如:
<%@ OutputCache Duration="300" VaryByParam="*"%>
2)然后,在缓存没有建立(或者已经过期)时,一部手机通过浏览器以WAP方式第一个请求了这个页面(Request headers中包含"Accept:text/vnd.wap.wml",这样的请求可能通过代码进行模拟,详见这里)。
3)接着,ASP.NET将这个请求的响应缓存了起来。这个操作是在System.Web.Caching.OutputCacheModule.OnLeave中进行的,缓存内容来自System.Web.HttpResponse.GetSnapshot(),不仅缓存了Response body,而且缓存了Response headers。
*【关键地方】在缓存Response headers时,ASP.NET根据"C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\Browsers\Default.browser"中的配置进行了特殊处理:
<defaultBrowser id="Wml" parentID="Default">
<identification>
<header name="Accept" match="text/vnd\.wap\.wml|text/hdml"/>
<header name="Accept" nonMatch="application/xhtml\+xml; profile|application/vnd\.wap\.xhtml\+xml"/>
identification>
<capabilities>
<capability name="preferredRenderingMime" value="text/vnd.wap.wml"/>
<capability name="preferredRenderingType" value="wml11"/>
capabilities>
defaultBrowser>
由于发起请求的Accept为text/vnd.wap.wml,所以上面的正好匹配,然后ASP.NET根据这里的配置,将Response headers中Content-Type的修改为"text/vnd.wap.wml; charset=utf-8",并将之缓存。
4)接下来的在缓存周期内的请求都是由这个缓存来迎接(操作在System.Web.Caching.OutputCacheModule.OnEnter中进行,最终由System.Web.HttpResponse.UseSnapshot返回缓存内容),只要不是WAP方式的请求,就会出现上面图中的下载文件对话框。
在解决这个问题时,我们试图捕捉这个响应,通过FireFox的Firebug, Chrome的Developer tools, Fiddler都没捕捉到,后来才试了试IE9的Developer tools,竟然捕捉到了(第一次享受到IE9带来的惊喜)。见下图:
问题的原因就在这里,正确的Content-Type应该是"text/html; charset=utf-8",ASP.NET却对WAP请求进行了特殊对待,由Accept决定Content-Type(难道是“屁股决定脑袋”)。可能是WAP的应用场景需要这样的设计,但在设计时却没有考虑对OutputCache的影响。
(注:我们测试了,将客户端请求的Accept改为其他类型,比如text/plain,都不存在这个问题)
从前面描述的过程中,我们可以知道,解决问题要在第3步提到的Default.browser文件下手。
推荐解决方法
前提条件:当前Web服务器上没有WAP站点
优点:一处更改,对当前Web服务器上的所有站点都有效。
操作步骤:
进入C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\Browsers
将Default.browser文件复制到桌面(另外再复制一份到另外一个文件夹进行备份)
用你喜欢的编辑打开桌面上的Default.browser文件,找到
部分,注释或者***以下配置并保存:
<capabilities>
<capability name="preferredRenderingMime" value="text/vnd.wap.wml"/>
<capability name="preferredRenderingType" value="wml11"/>
capabilities>
将修改过的Default.browser文件复制至"C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\Browsers",覆盖同名文件。
以管理员身份运行命令行,cd进入"C:\Windows\Microsoft.NET\Framework\v4.0.30319",运行命令"aspnet_regbrowsers -i":
大功造成,然后就可以尽情地进行OutputCache。
注:该解决方法基于之前的经验 —— ASP.NET4中不要相信Request.Browser.Cookies,Form验证要用UseCookies,如果当时不写博客,找到这个解决方法就没这么容易了。
知道了原因,解决方法有多种,我们这里只列出我们将采用的解决方法。
ASP.NET MVC呢?
在ASP.NET MVC中没有这个Bug,前一文章中提到的“浏览器缓存的Bug”在ASP.NET MVC中也不存在。
小结
1)为什么ASP.NET有这些Bug而ASP.NET MVC没有?
我想其中一个重要的原因就是ASP.NET的封闭,ASP.NET MVC的开放(开源)—— 有了源代码,开发者发现问题时可以更快地找到真正的原因,然后反馈给微软。即使微软不能立即解决问题,至少我可以修改源代码自己解决问题,有了这个条件,开发者遇到问题才愿意追要究底。不然,即使找到了问题的原因,由于不能修改源代码,只能望问题心叹(比如,我之前遇到的Entity Framework问题)。ASP.NET MVC的开放是它成功的关键,Entity Framework是否成功也将取决于它是否开放。
2)ASP.NET WebForms与ASP.NET MVC会并存吗?
以前我觉得会并存,但现在ASP.NET WebForms的Bug微软自己都懒得去解决,你还相信会并存吗?