一個古老問題,在 ASP.NET 呼叫 Response.End() 會觸發 ThreadAbortException,假警報常會干擾偵錯與問題追查,之前寫過文章但沒整理完整的替代方案,今天補上筆記。
使用以下程式重現問題,WebForm 網頁包含一枚按鈕,按下時透過 AJAX 呼叫同一程式,Page_Load() 事件遇 Request["m"] == "ajax" 時 Response.Write() 傳回 Guid 並以 Response.End() 中止程式,避免傳回 HTML 內容:
<%@ Page Language="C#" %>
<scriptrunat="server">
void Page_Load(object sender, EventArgs e)
{
if (Request["m"]=="ajax")
{
returnGuid();
}
}
void returnGuid()
{
Response.Write(Guid.NewGuid().ToString());
Response.End();
}
</script>
<html>
<body>
<button type="button">Get GUID</button>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script>
$("button").click(function() {
$.post("TestRespEnd.aspx", { m: "ajax" }).done(function(res) {
alert(res);
});
});
</script>
</body>
</html>
如下圖所示,即使程式可正確執行,當使用 Visual Studio偵錯時 Response.End() 會觸發 ThreadAbortException 造成中斷。若 Response.End() 被 try catch包覆,則會進入 catch 流程。
如前文所提,官方建議解法是改用 CompleteRequest() 但沒有交待細節。參考 stackoverflow 討論,找到一則完整處理範例,步驟是先 Response.Flush() 將先前 Response.Write() 寫入內容傳回客戶端,接著設定 Response.SuppressContent = true 防止再傳回其他內容,最後使用 CompleteRequest() 略過 ASP.NET Pipeline 其他步驟直接跳至 EndRequest() 事件。程式範例中的 NoExceptionResponseEnd() 方法使用 HttpContext.Current.Response,可置於程式庫共用,不限定要寫進 WebForm .aspx.cs,使用時將 Response.End() 改成 NoExceptionResponseEnd() 並補上中止執行邏輯。
void Page_Load(object sender, EventArgs e)
{
if (Request["m"]=="ajax")
{
returnGuid();
//Response.End()會停止Thread,不必煩惱後方還有邏輯
//若確定函式己中止Response,要防止後方程式繼續執行
return;
}
}
void returnGuid()
{
Response.Write(Guid.NewGuid().ToString());
NoExceptionResponseEnd();
}
publicvoid NoExceptionResponseEnd()
{
//https://stackoverflow.com/a/22363396/288936
//將Buffer中的內容送出
HttpContext.Current.Response.Flush();
//忽視之後透過Response.Write輸出的內容
HttpContext.Current.Response.SuppressContent = true;
//忽略之後ASP.NET Pipeline的處理步驟,直接跳關到EndRequest
HttpContext.Current.ApplicationInstance.CompleteRequest();
}
不過有一點要特別留意:NoExceptionResponseEnd() 與 Resonse.End() 最大的差異在於 Response.End() 會中止目前的執行緒,故 Response.End() 之後的程式碼一定不會被執行;NoExceptionResponseEnd() 則不然,我們必須自行中止程式。在本例中可藉由 return 退出避免執行 Page_Load() 下半段程式,如果忘了 return 而後面又試圖輸出 Response 就會出錯(如下圖),更麻煩的一種狀況是 Response.End() 改用 NoExceptionResponseEnd() 後忘了自行中止程式,執行了原本不該執行更新資料庫或寫檔案動作,有可能破壞商業邏輯,故換掉 Reponse.End() 時務必要謹慎。
另外有一種狀況是 Response.End() 寫在外部函式裡,在特定條件下才會中止 Response,原本函式使用 Reponse.End() 呼叫端不需煩惱是否繼續執行下去(反正Response.End()後程式就停了),改用 NoExceptionResponseEnd() 後則要加上判斷,一個簡單做法是依Response.SuppressContent 決定是否繼續。
if (Request["m"]=="ajax")
{
returnGuid();
//如果不確定函式內部是否己中止Response
//可透過Response.SuppressContent簡易判斷
if (Response.SuppressContent)
return;
}
簡單總結:
- 使用 Response.Flush() + 設定 Response.SuppressContent + CompleteRequest() 可以模擬 Response.End() 並避免觸發 ThreadAbortException。
- Reponse.End() 與替代做法最大的差異是 Response.End() 會中止執行緒,我們不用費心後面的程式碼還要不要執行。改掉 Response.End() 時務必謹慎檢查,避免觸發原本不該執行的程式邏輯。
- 若外部函式原本在某些情況下會 Response.End(),呼叫端可檢查 Response.SuppressContent 簡易判斷決定是否繼續。