The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

ASP.NET MVC 使用 Html.BeginForm 預設多載引發的問題

最近因為有客戶要求將網站部署到 Windows Azure WebSites 雲端平台,原本在開發環境測試都沒什麼狀況,反而是部署到 Windows Azure WebSites 之後才發現一個棘手的問題,一般網頁瀏覽並不會有問題,而是當使用 IE 瀏覽器,且有表單要送出資料時就會發生 502 - Web server received an invalid response while acting as a gateway or proxy server. 的問題,這才發現一個 ASP.NET MVC 的小臭蟲(Bug)。

※ 問題描述

這問題發生的機會並不高,因為我整個網站都用「中文」命名,所有 Controller 的名字都是中文,例如:

  • 首頁Controller
  • 會員專區Controller
  • 最新消息Controller

基本上用了中文當作 Controller 名稱並不會有什麼問題,只要 URL 格式符合標準即可,也就是只要有適當的 UrlEncode 處理就能正常運作。

我們先來看看 ASP.NET MVC 的表單頁面宣告如下,我們使用 Html.BeginForm() 預設的第一個多載,也就是不傳入任何參數的版本:

@using (Html.BeginForm())
{
    @Html.TextBox("Username")

    <input type="submit" />
}

這個表單最後輸出的 HTML 如下:

<form action="/最新消息/Create" method="post">
    <input id="Username" name="Username" type="text" value="" />
    <input type="submit" />
</form>

你可以發現,在表單的 action 屬性中出現了「最新消息」這段中文字,基本上,直接傳送 UTF-8 文字編碼的網址到 IIS 伺服器,在大部分的情況下並不會有問題,只有在特定條件下才會導致錯誤發生,其分析如下。

發生 502 - Web server received an invalid response while acting as a gateway or proxy server. 這個問題的主因並不在「網址列」,而是在 HTTP 要求標頭中的 Referer 標頭 (Header) 出現了不合法的字元,也就是這裡出現了「沒有 URL 編碼過的網址」,進而引發錯誤:

POST /最新消息/Create HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Referer: http://example.azurewebsites.net/最新消息/Create
Accept-Language: zh-TW,en-US;q=0.5
User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
Proxy-Connection: Keep-Alive
Content-Length: 12
DNT: 1
Host: example.azurewebsites.net
Pragma: no-cache
Cookie: ARRAffinity=f3c3d0f49d36fdfaf7d19292e1acb191ff9ac55c9c32f176e8f66d8f87fd73f4

你可能會想問說,IIS 遇到出現有中文的 HTTP Headers 就會掛掉嗎?其實並不是 IIS 的問題,而是因為 Windows Azure WebSites 服務都是用靠 URL RewriteApplication Request Routing 這兩個模組來負責 Reverse Proxy 的工作(主要用途是做負載平衡),而這兩個模組遇到不合法的 HTTP Headers 就會回報 HTTP 502 的錯誤!

 

除了這個問題外,我也因為使用了 Ajax.BeginForm 輔助方法進而發現了另一個問題,因為瀏覽器會自動修正無效的網址列編碼,所以當表單的 action 屬性中的網址沒有做 URL 路徑編碼 (UrlPathEncode) 的話,瀏覽器還是會自動幫你做編碼,不過 URL 編碼只是一種「編碼方法」,他並沒有指明說要用什麼「文字編碼」來做 UrlEncode,在所有「非 IE 瀏覽器」裡,大家都會用 UTF-8 當成預設的文字編碼,但在 IE 瀏覽器之中,除了 IE10 比較標準外,IE9 以下的瀏覽器版本,全部都會依據作業系統的系統地區設定來自動決定編碼,當我們安裝「中文 (台灣)」時,文字編碼自動選擇 Big5 編碼,而這個 Big5 編碼的文字再經過 UrlEncode 編碼之後,才會傳到伺服器端。

如果你的伺服器上的系統地區設定是「中文 (台灣)」的話,程式也完全不會出問題,IIS 可以順利的解析所有文字編碼的問題。但是我們這次遇到的問題卻是因為網站要部署到 Windows Azure WebSites 雲端平台,而這些雲端平台上的主機,系統地區設定都設定為「英文 (美國)」,也因此當用戶端傳送 Big5 與 UrlEncode 編碼過的網址時,就無法正確解碼了,也因此發生 HTTP 404 錯誤,且只有使用 IE9 以下的瀏覽器版本才會出現這個問題!

 

解決之道

解決這個問題,就是讓表單的 action 屬性中的網址路徑先經過 UrlPathEncode 編碼,在 .NET 底下,預設都是以 Unicode 做編碼,因此透過 .NET 先行編碼,再輸出到 HTML 裡,就可以確保輸出的網址路徑一定是在 Unicode 的文字編碼下做的 UrlPathEncode 結果,如此一來,不同瀏覽器之下發出的表單要求就可以一致,解決不同瀏覽器與版本之間的相容性問題。

在 ASP.NET MVC 的 View 裡,不需要對網址路徑特別做 UrlPathEncode 編碼,本文一開始我就講到,這應該算是 ASP.NET MVC 的一個小臭蟲(Bug),而且是只有當使用 Html.BeginForm() 且不傳入任何參數時才會導致表單輸出的 action 屬性內容沒有經過 UrlPathEncode 編碼。也就是說,只要用其他的多載,就可以解決這個問題,在這裡,我的解決方式是使用 FormExtensions.BeginForm Method (HtmlHelper, Object) 這個多載,也就是傳入一個 routeValues 物件,範例程式如下:

@using (Html.BeginForm(new { 
    action = this.ViewContext.RouteData.Values["action"].ToString() }))
{
    @Html.TextBox("Username")

    <input type="submit" />
}

上述程式的 HTML 輸出的結果如下:

<form action="/%E6%9C%80%E6%96%B0%E6%B6%88%E6%81%AF/Create" method="post">
    <input id="Username" name="Username" type="text" value="" />
    <input type="submit" />
</form>

如此一來,問題便迎刃而解。

留言評論