同事回報了一起奇怪狀況,追查之後又學到新東西。在我的觀念裡,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表單登入認證&授權 - 分享是一種學習 - 點部落

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

Find out why Response.End() doesn't stop ASP.NET MVC execution by tracing the source code.


Comments

# by woshizilong@hotmail.com

黑哥,您好。一直關注您的部落格,學到很多東西。 今天遇到一個奇怪的問題,不知道您有沒有興趣。 old server: windows server 2008 R2 + iis7 + mysql5 new server: windows server 2012 R2 + iis8 + mysql8 project: 很久前用vs2008 編寫的 asp.net mvc2 專案 我在新伺服器上配置了該專案,程式執行正常,從log看sql 的 insert select update 等操作一切正常,畫面返回結果也是正常的。但是當網頁中有檔案upload的post後,select返回結果就不對了,應該有查詢結果的select返回的記錄數是0。 重新啟動伺服器之後,select的查詢又恢復正常了。 檔案upload會影響資料庫連線的查詢?像是伺服器配置的問題,您有遇到過類似問題嗎?

# by Jeffrey

to woshizilong, 聽起來頗詭異。我想到的調查方向是SELECT查詢先加上Log記錄結果筆數,筆數為零時手動連上MySQL也執行相同SELECT看結果是否一致,再進一步研究。

# by woshizilong@hotmail.com

黑哥,出現問題後,Log裡的結果筆數是0,但是我用mysql執行卻有結果的。我還嘗試過把MySQL從8降到MySQL5,問題依舊。目前正在調查II8對舊版.net編譯的網站部署有哪些注意點。

# by Jeffrey

to woshizilong, 若確定Log記錄到的查詢與手動查詢使用的SELECT完全一致但結果不同,我口想到一種可能:INSERT與SELECT共享一個資料庫Session,零筆屬於未Commit的結果,故與外部SELECT(不允許Dirty Read)得到的結果不同,重啟更新動作被Rollback,故Web出現再次非零筆。但這個推論要剛好資料邏輯情境剛好落入前述條件才成立,機會應不高。

# by woshizilong@hotmail.com

TO 黑哥,今天重新部署了一次伺服器,把MySQL8替換成了原伺服器上的Mysql一致的版本MySQL5.0.0.1b (x86)重新安裝後。問題消失了😓。資料驅動用MySQL.Data.dll 可能是這個老版本不相容相對新的MySQL5.7和MySQL8.0。

# by Jeffrey

to woshizilong, 不相容居然會產生這種詭異現象,開眼界了,謝謝分享。

Post a comment