在 ASP.NET MVC Response.End() 不會中斷執行

同事回報了一起奇怪狀況,追查之後又學到新東西。在我的觀念裡,Response.End() 時會立即中斷執行,有時還會觸發討厭的 ThreadAbortException。但在以下的 ASP.NET MVC 範例中,CheckAuth() 在查不到 Cookie 時會導向 /Login 並呼叫 Response.End() 結束執行,結果沒有,程式繼續往下跑,在試圖修改 Response.ContentType 時觸發 HTTP Header 送出後無法修改 ContentType 的錯誤:(修改 ContentType 需求來自 JsonNetResult )

Response.End() 不會中斷執行?這大大違背我的認知,莫非 WebForm 與 ASP.NET MVC 的行為不同?真相在原始碼裡,Use the source, Luke!

追進 HttpResponse 原始碼,很快有了答案:

        /// <devdoc>
        ///    <para>Sends all currently buffered output to the client then closes the
        ///       socket connection.</para>
        /// </devdoc>
        public void End() {
            if (_context.IsInCancellablePeriod) {
                AbortCurrentThread();
            }
            else {
                // when cannot abort execution, flush and supress further output
                _endRequiresObservation = true;

                if (!_flushing) { // ignore Reponse.End while flushing (in OnPreSendHeaders)
                    Flush();
                    _ended = true;

                    if (_context.ApplicationInstance != null) {
                        _context.ApplicationInstance.CompleteRequest();
                    }
                }
            }
        }

原來 Reponse.End() 時會依據 IsCancellablePeriod 屬性決定是否中斷執行緒。由 IsCancellablePeriod 關鍵字追到 Response.End()在Webform和ASP.NET MVC下的表现差异 - 空葫芦 - 博客园,證實了 Reponse.End() 在 WebForm 與 MVC 的行為不同。有趣的是,在該文發現保哥也追過這個問題,IsCancellablePeriod 取決於  _timeoutState 屬性值,在 WebForm 下其值為 1 (IsCancellablePeriod = true),在 ASP.NET MVC 下為 0 (IsCancellablePeriod = false),故 Response.End() 在 WebForm 下會執行 AbortCurrentThread() 在 MVC 則是 Flush() 並執行 ApplicationInstance.CompeteRequest()。

如此即可解釋 Response.End() 後會繼續執行且無法修改 ContentType。(因為在 End() 中已 CompleteRequest() )

既知原因,來看如何解決。最粗暴但有效的解法是自己模擬 AbortCurrentThead() 中止執行,HttpResponse.AbortCurrentThread() 原始碼是呼叫 Thread.CurrentThread.Abort(new HttpAppication.CancelModuleException(false));,所以我們將程式碼修改如下即可搞定。
(此舉如同 Reponse.End() 會有觸發 ThreadAbortException 的副作用,參考:ThreadAbortException When Response.End() - 黑暗執行緒)

        void CheckAuth()
        {
            //模擬Cookie檢查
            if (Request.Cookies["AuthCookie"]?.Value != "X")
            {
                Response.Redirect("/Login");
                Response.End();
                Thread.CurrentThread.Abort();
            }
        }

另一個思考方向是 CheckAuth() 改傳回 bool,呼叫時改寫成 if (CheckAuth()) { …認證成功作業... } else { return Content(null); },但如此一來,所有用到 CheckAuth() 的 Action 都要多一層 if,噁心又麻煩,不優。

而依此案例的認證需求,倒是可以回歸 ASP.NET 內建的表單驗證。CodeProject 有篇文章可以參考:A Beginner's Tutorial on Custom Forms Authentication in ASP.NET MVC Application - CodeProject,先設定 web.config

<authentication mode="Forms">
  <forms loginUrl="~/Login" timeout="2880" />
</authentication>

/Login 認證身分成功後呼叫 FormsAuthentication.SetAuthCookie(username, false); 連自訂認證 Cookie 的功夫都免了。

如果認證邏輯再複雜,則可考量實作 IAuthenticationFilter 實現自訂認證。例如:[ASP.NET MVC]使用IAuthenticationFilter,IAuthorizationFilter實作Form表單登入認證&授權 - 分享是一種學習 - 點部落

追進原始碼,學到新東西,敲開勳~

歡迎推文分享:
Published 24 August 2018 10:52 PM 由 Jeffrey
Filed under:
Views: 2,447



意見

沒有意見

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<August 2018>
SunMonTueWedThuFriSat
2930311234
567891011
12131415161718
19202122232425
2627282930311
2345678
 
RSS
創用 CC 授權條款
【廣告】
twMVC

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication