一個古老問題,在 ASP.NET 呼叫 Response.End() 會觸發 ThreadAbortException,假警報常會干擾偵錯與問題追查,之前寫過文章但沒整理完整的替代方案,今天補上筆記。

使用以下程式重現問題,WebForm 網頁包含一枚按鈕,按下時透過 AJAX 呼叫同一程式,Page_Load() 事件遇 Request["m"] == "ajax" 時 Response.Write() 傳回 Guid 並以 Response.End() 中止程式,避免傳回 HTML 內容:

<%@ Page Language="C#" %>
<script runat="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();
    }
 
    public void 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 簡易判斷決定是否繼續。

Comments

Be the first to post a comment

Post a comment