解掉一個古老懸案!

在ASP.NET MVC 4中使用Kendo UI Grid文章曾提過一個古怪問題:

發現StyleBundle的virtualPath參數出現2012.1.322時,會導致Styles.Render("~/Content/kendo/2012.1.322/css”)時傳回HTTP 404錯誤~ 為克服問題,我將2012.1.322目錄的內容向上搬一層,直接放在~/Content/keno目錄下,並將virtualPath設成"~/Content/kendo/css"避開問題。

但說來奇怪,問題後來好像自行消失,之後寫了好多專案,Bundle寫成Styles.Render("~/Content/kendo/yyyy.m.nnn/css”)都未再出狀況,這條密技也漸被淡忘。

最近接到同事回報,問題在某Windows 2003測試台出現,症狀很像:將檔案向上搬一層,或將路徑由2012.1.322改成2012_1_322或2012-1-322都可避開。很神奇地,若使用IIS Express或本機IIS7,即便用2012.1.322路徑也不會有任何問題。由這些線索,可歸納出以下幾點:

  1. 問題只出現在Windows 2003 IIS6,在Windows 7/2008R2 IIS7及IIS Express上不會發生。
  2. 另一個構成問題的條件是路徑中出現"."!將"."改為"-"或"_"就沒事。

為了調查,我建了一個MVC小專案,在其中使用Styles.Render("~/Content/kendo/2012.1.322/css”),將專案部署到出問題的Windows 2003主機,居然也沒出錯。跟同事要來問題專案Source,抽離掉無關模組,只留下一個cshtml引用Styles.Render("~/Content/kendo/2012.1.322/css”),部署到問題主機,你猜怎麼著,一切正常。一發狠,將整個問題專案檔案搬到為測試另建的IIS Web Appliation資料夾,噗!問題也沒出現。

經過這番比對,茶包呼之欲出 - 關鍵應在IIS的網站應用桯式設定上!很快地,一把抓出這枚躲了兩年多的茶包:

問題網站少了萬用字元應用程式對應(需加入的理由可參考保哥文章的常見問題3),而新建測試網站有加,這解釋檔案搬移到測試網站目錄何以問題消失。但有一點與我的認知不同,記得在古早時代,少了這條連/home/index都會不會通,但問題網站沒設定也跑得很好,只有KendoUI CSS Bundle出問題,莫非是新版MVC的魔法?

在一篇MSDN Blog找到答案:

In v4.0 there is a new feature that allows extensionless URLs to be directed into managed code, without impacting static requests (HTML, JPG, GIF, CSS, JS, etc).  Because of this feature, on IIS 6 you no longer need a wildcard mapping and on IIS 7 you no longer need to set runAllManagedModulesForAllRequests=”true” or remove the "managedHandler" precondition for the UrlRoutingModule.  It works by default on both IIS 6 and IIS 7, except that you need a QFE from the IIS team to make this work on Windows Vista SP2, Windows Server 2008 SP2, Windows Server 2008 R2, and Windows 7.  Once you obtain the IIS 7 QFE and install v4.0 ASP.NET, you’ll be able to route extensionless URLs without impacting static requests.  The QFE enables a new “*.” handler mapping—the notation may seem weird, but all you care about is the fact that this maps to URLs without an extension.  ASP.NET registers a “*.” handler mapping when v4.0 is installed.  If you don’t have the IIS 7 QFE, that handler mapping does nothing.  If you have the IIS 7 QFE, extensionless URLs are mapped to our handler, which enables them to be routed by the UrlRoutingModule.  Information about the IIS 7 QFE and steps to download it can be found at http://support.microsoft.com/kb/980368.  For the record, the implementation of this feature on IIS 6 is done quite differently, and I won't go into that here.

簡單來說,ASP.NET 4.0註冊了一個副檔名為"*."的對應,而IIS6/7更新後也實做了"*."的對應邏輯,藉以將無副檔名URL交給ASP.NET ISAPI處理(例如:ASP.NET MVC的/home/index、ScriptBundle/StyleBundle的"bundles/jquery"、"~/Content/kendo/2012.1.322/css"都屬於無副檔名URL),比過去使用萬用字元對應將html、jpg、css、js等靜態檔也一併納入有效率多了!

這次遇到的問題,應屬IIS6處理*.對應邏輯的瑕疵,當路徑出現"."時,會落入原有的IIS靜態檔案邏輯,因而得到HTTP 404回應。為了驗證這點,我做了一個小實驗,在RouteConfig加入一個包含"."的特殊URL "foo.bar/{id}:

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.IgnoreRoute("favicon.ico");
 
        routes.MapRoute(
            name: "Foo",
            url: "foo.bar/{id}",
            defaults: new { controller = "Home", action = "Foo", id = UrlParameter.Optional }
        );
 
        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

HomeController則加入Foo Action:

    public class HomeController : Controller
    {
        public ActionResult Index(string culture)
        {
            return View();
        }
 
        public ActionResult Foo(string id)
        {
            return Content("Foo: " + id);
        }
    }

本機測試OK:

部署到IIS6,/home/index正常,一如預測,foo.bar/Blah出現HTTP 404:

在IIS6加入萬用字元應用程式對應後運作正常,證實了我們的推測!

【結論】

IIS6的無副檔名URL處理邏輯在遇路徑有"."(dot)時會失效,導致Styles.Render("~/Content/kendo/2012.1.322/css”)出現HTTP 404錯誤,若不調整路徑,需加入萬用字元應用程式對應解決(但會因所有靜態檔都經過ASP.NET ISAPI影響效能),此問題在IIS7+則不會發生。另外,若ASP.NET 4.0在IIS6未遇URL有"."的特殊狀況,已不需設定萬用字元應用程式對應。

PS:我沒能找到官方文件佐證,在ASP.NET論壇找到一篇討論,MS員工回覆相似的結論
I think with IIS 6, your only solution is going to be to set up a wildcard extension.  The decimal is going to be seen by IIS 6 as an extension, so setting up the extensionless url feature won't address the issue. 另外,回覆還提到明確指定由StaticFileHandler處理css, js等靜態資源請求,可以減緩萬用字元應用程式對應造成的效能影響,值得參考。


Comments

# by kcw

Jeff大, MVC5 在RegisterRoutes加入以下設定: routes.MapRoute( name: "Download", url: "Download/{file}", defaults: new { controller = "Home", action = "Download", file = UrlParameter.Optional } ); HomeController 加入: public ActionResult Download(string file) { Debug.WriteLine(string.Format("Attempt to download the file which name is : {0}.", file)); return View("Index"); } 然後進行Debug並進入這三條連結: 1. http://localhost:xxxxx/Download?file=excel.xlsx 2. http://localhost:xxxxx/Download/excel.xlsx 3. http://localhost:xxxxx/Download/excel. 3. http://localhost:xxxxx/Download/excel 其中第1和第4的連結有效,而第2和第3的連結會造成Http 404。 請問是我的MapRoute設定有問題嗎? 參考:Windows 8, IIS Express, VS2013, MVC5

# by Jeffrey

to kcw, 要在URL的結尾使用"."或".*",必須強迫IIS不要將其解析成靜態檔,最簡單做法是加上以下設定: <system.webServer> <modules runAllManagedModulesForAllRequests="true" /> </system.webServer> 但這會有連.js, .css, .html都經由.NET處理的效能下降議題。要避免的方法有好幾種,文中有提到指定StaticFileHandler,Scott Hanselman也有篇文章 http://bit.ly/11Dw3uE。 再不然,避開在URL結尾出現"."也是做法,例如URL改成 url: "file/{file}/download"。

Post a comment