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