Introduction

The story began very simply.

One day, while started working, I got an error mail from one of our deployed Asp.net application in Production site. The error mail was something like the following:

Timestamp: 7/2/2010 7:26:15 PM
Message: HandlingInstanceID: 71393a92-8cd9-42f5-9572-ca172ae0ca03
An exception of type 'System.Threading.ThreadAbortException' occurred 
and was caught.

Type : System.Threading.ThreadAbortException, mscorlib, 
Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 
Message : Thread was being aborted.
Source : mscorlib
Help link :
ExceptionState : System.Web.HttpApplication+CancelModuleException
Data : System.Collections.ListDictionaryInternal
TargetSite : Void AbortInternal()
Stack Trace : at System.Threading.Thread.AbortInternal()
at System.Threading.Thread.Abort(Object stateInfo)
at System.Web.HttpResponse.End()
at System.Web.HttpResponse.Redirect(String url, Boolean endResponse)
at System.Web.HttpResponse.Redirect(String url)
at MyDashboard.Page_Load(Object sender, EventArgs e)

Guess what, we have a simple exception reporting system at each of our Asp.net production sites. Whenever an un-handled exception occurs there, the exception information is captured and emailed to a pre-configured email address so that we can investigate and fix it.

To do this, we usually put a few lines of codes in the Global.asax, which catches any un-handled exception and emails the exception details to us. Here is the code:

void Application_Error(object sender, EventArgs e)
{ 
        // Code that runs when an un-handled error occurs
        Exception ex = Server.GetLastError();
        Logger.LogException(ex);
} 

So, the above exception information was being emailed by this tiny little friendly code, which saves our life often.

Why this exception occurred?

There must be some un-handled exception occurring behind this error report. While starting investigation,  I noticed that, a Response.Redirect() statement is behind this exception. See the call stack information from top to bottom:

at System.Threading.Thread.Abort(Object stateInfo)
at System.Web.HttpResponse.End()
at System.Web.HttpResponse.Redirect(String url, Boolean endResponse)
at System.Web.HttpResponse.Redirect(String url)
at MyDashboard.Page_Load(Object sender, EventArgs e) 

Analyzing the MyDashboard.aspx.cs, I found that there is indeed a Response.Redirect() statement, which looks pretty harmless to me:

if(!UserHasPermissionToViewThisPage())
{
    Response.Redirect("Unauthorized.aspx");
} 

Hm..seems interesting. It seems, the Response.Redirect() statement is causing something which is generating the ThreadAbortException.

The call stack gives some more information. Let's look at it. The Response.Redirect() internally executes some more methods in the following sequence:

Response.Redirect(url,endResponse)-->Response.End()-->Thread.Abort()-->Boom! (ThreadAbortException)

So, it appears, the Response.Redirect() internally calls Resposne.End(), which calls Thread.Abort() and this last method invocation causes the ThreadAbortException to be thrown.

ThreadAbort.png 

Figure : Sequence of operations that causes ThreadAbortException

Why ThreadAbortException is being thrown?

Well, a little research revealed me that, the Response.Redirect() is actually an instruction to the client browser to stop the execution of the current page and hit a different URL request. As each and every request is being assigned to a different Thread from the Thread pool, the current Thread (Which is taking care of the current Request) should be terminated (And, go back to the Thread pool) and the next redirected Request should be handled by a different Thread pool.

MSDN Says :

"Thread.Abort() Raises a ThreadAbortException in the thread on which it is invoked, to begin the process of terminating the thread. Calling this method usually terminates the thread."

ThreadAbortException.png 

Figure : Child Thread raising ThreadAbortException to terminate itself

So, it turns out that, when Response.Redirect() is being invoked, the current Thread can't terminate itself, and hence, it needs a way to notify the parent Thread which created this Thread (The Asp.net worker process in this case) so that, the parent Thread can terminate this thread. Calling Thread().Abort() actually raises an Exception (ThreadAbortException) to the parent Thread to notify that "I can't terminate myself, you please do it" , and, the parent Thread terminates this Thread and returns it back to the Thread pool.

So, this is the story behind the ThreadAbortException.

Can we avoid this ThreadAbortException? 

Yes we can! It's pretty simple actually. The Response.Redirect() has an overloaded version which lets you specify whether or not to end the Response while redirecting the current Request. Following is the overloaded  version of the method:

Response.Redirect(string url, string endResponse); 

So, if we invoke the method with endResposne=false, the Response.Redirect() will not call the Response.End(), and, in turn, the Thread.Abort() method will not be called, and, no ThreadAbortException will occur. Problem solved. Bravo!

I tried it, and unfortunately found that, it stopped the ThreadAbortException, but, created a new problem. See the following piece of code (Not the best piece of code you would ever see, but, it demonstrates the problem):

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!UserHasPermissionToViewThisPage())
        {
            Response.Redirect("Unauthorized.aspx",false);
        }

        DeleteFile();
    }

    private void DeleteFile()
    {
        string fileName = Server.MapPath("TextFile.txt");
        File.Delete(fileName);
    }

    private bool UserHasPermissionToViewThisPage()
    {
        return false;
    }
}

The code says, if the current user doesn't have permission to execute the functionality of this page,redirect him/her to the "Unauthorized.aspx". On the other hand, if he/she has permission, executes rest of the codes for the current user, and, ultimately, delete a file in the file system in this particular case.

Put a text file (TextFile.txt) at the web root folder of your Asp.net web site/application, copy/paste the above code in the CodeBehind file of a simple aspx page, browse it and see what happens.

As soon as you browse your page, you are being redirected to the Unauthorized.aspx page. Fine, this is expected. But, where is the TextFile.txt? OMG, it's gone!

It seems, even if the Response.Redirect() is being executed here (And, the browser is successfully being redirected to a new URL), the later part of the codes is getting executed here. That is, the following code is being executed:

DeleteFile();

I changed the following Response.Redirect() statement (removed the endResponse parameter) and ran the page in the browser to see what happens now (Also, restored a blank TextFile.txt file in the web root folder)

Response.Redirect("Unauthorized.aspx");

Interestingly, the TextFile.txt is not deleted now, and obviously the DeleteFile(); statement is not being executed here after the Response.Redirect() statement.

I gave a thought and concluded that, whatever happening here with Response.Redirect(url, false) (Note, endResposne parameter is set to false) is logical. Why? because, when I send the endResponse parameter value as false, Response.Redirect() doesn't call Response.End(), and hence, the Thread.Abort() is not being invoked here, resulting in the current Thread (Which is taking care of the current Request) being live and executing the other parts of the statements, whatever it does!

Dude, we have a problem here.

How to prevent the current Thread executing the other codes?

Simple, we just need to add a return statement just after the Response.Redirect() statement, right?

Response.Redirect("Unauthorized.aspx",false);
return; 

Yes, this solves the problem. But, as I found that, there are still issues with PostBacks. The above code may return immediately after Redirecting to a different URL, but, if the current Request is a PostBack Rerquest, the PostBack event methods are getting executed without any interruption. I added a Button server control to the page, added an OnClick event method to it, and, explored the issue. Take a look at the following codes which demonstrates the problem:

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (IsPostBack)
        {
            if (!UserHasPermissionToViewThisPage())
            {
                Response.Redirect("Unauthorized.aspx", false);
                return;
            }
        }
        //This no longer gets called now
        DeleteFile();
    }

    private void DeleteFile()
    {
        string fileName = Server.MapPath("TextFile.txt");
        File.Delete(fileName);
    }

    private bool UserHasPermissionToViewThisPage()
    {
        return false;
    }

    protected void Button1_Click(object sender, EventArgs e)
    {
        //But, this is getting called even if Response.Redirect() is being called!
        DeleteFile();
    }
}

Further exploration revealed me that, even if Response.Redirect(url, false) is supposed to hault the execution of the current page, the whole Asp.net page life cycle events gets executed here without any interruption. So, at the end of the life cycle, the Response is unnecessarily being written to the output stream even if the client browser is already redirected to a different URL.

So, it seems, returning just after the Response.Redirect() is not good enough. We need to track the redirection information somehow and prevent the execution of codes within the PostBack event methods if the flag's value is true (Indicating that, Page alredy redirected). We can use a global flag variable as follows:

bool Redirected = false;
...
...
if (!UserHasPermissionToViewThisPage())
{
           Response.Redirect("Unauthorized.aspx", false);
           Redirected = true;
           return;
}

protected void Button1_Click(object sender, EventArgs e)
{
        if(Redirected) return;
        DeleteFile();
} 

As it turns out, we need to check this flag in each and every PostBack event method in our CodeBehind methods. Doesn't sound good to me.

A smarter approach

Asp.net Page life cycle is good. It gives you flexibility to override most of the life-cycle events, and, execute your own piece of code where you want in the specific part of the life cycle.

So, I might try to get rid of the fact that, I need to check the flag inside each and every PostBack event method.

As the Page Life cycle says, the RaisePostBackEvent event is fired before firing the specific PostBack event method which is tied to a server control's specific event (In this case, Button1_Click event method). So, we can override this method, and, check the flag within the overridden method, and, let Asp.net run time invoke the original ReaisePostBackEvent() method if the flag's value is false only.

Following is the code I wrote:

protected override void RaisePostBackEvent(IPostBackEventHandler sourceControl, string eventArgument)
{
        if (Redirected == false)
            base.RaisePostBackEvent(sourceControl, eventArgument);
}

Adding to this, we also need to override the Render() event method so that, the Response is not unnecessarily being written to the output stream when we call Response.Redirect(url, false):

protected override void Render(HtmlTextWriter writer)
{
        if (Redirected == false)
            base.Render(writer);
} 

It would be even smarter to define these two methods and a Redirect() method within a base page class, and, let all other CodeBehind classes inherit this class. This would let us write less codes and keep the original PostBack event methods clean. That is:

public class BasePage : System.Web.UI.Page
{
    protected bool Redirected = false;

    public BasePage()
    {
    }
  
    protected void Redirect(string url)
    {
        Response.Redirect(url, false);
        Redirected = true;
    }

    protected override void RaisePostBackEvent(IPostBackEventHandler sourceControl, string eventArgument)
    {
        if (Redirected == false)
            base.RaisePostBackEvent(sourceControl, eventArgument);
    }

    protected override void Render(HtmlTextWriter writer)
    {
        if (Redirected == false)
            base.Render(writer);
    } 
}

public partial class _Default : BasePage
{
    if (IsPostBack)
    {
            if (!UserHasPermissionToViewThisPage())
            {
                Redirect("Unauhorized.aspx");
                return;
            }
    }
    //This no longer gets called now
    DeleteFile();
   ....
}

So, to summarize, following are the things you have to do:

  • Create a BasePage and include the methods as as suggested above. Inherit the BasePage from all other CodeBehind pages.
  • When you need to redirect, DO NOT CALL Response.Redirect(). Instead, call the following piece of code:
base.Redirect(url);
return;

Criticisms to this approach

Despite the fact that the above approach gives you some cleaner approach of avoiding the ThreadAbortException from happening, there are a few unresolved issues. These are:

  • The current Thread is not being aborted immediately. Rather, it stays alive which is unnecessary.
To me, this is not a big problem, because, though the Thread isn't getting terminated immediately, it will execute and will be returned back to the ThreadPool as it completes the page life cycle (Without really doing anything else, because, the Redirected flag is true). So, Asp.net worker process will not fall in Thread starvation.
  • It enforces a constraint to write a return statement just after the base.Redirect() method, which may not be easy to follow everywhere.
To me, this is a big problem. Specially, writing just a return statement may not be a straight-forward solution always, specially when you need to redirect from within a custom-written method which is not a page life cycle method. Take a look at the following piece of code:
protected void Page_Load(object sender, EventArgs e)
{
        if (!DenyIfUserDoesntHavePermissionToViewThisPage())
        {
            return;
        }
}

private bool DenyIfUserDoesntHavePermissionToViewThisPage()
{
        bool userHavePermission = false;

        if (!userHavePermission)
        {
            base.Redirect("Unauthorized.aspx");

            return false;
        }

        return userHavePermission;
}
You see, we need to return false from within the custom method DenyIfUserDoesntHavePermissionToViewThisPage() (Where we are redirecting to a different URL), and, we also need to return false from within this method to the caller. If the caller method is being called from another method, we need to return false to that method also (From within the calling method), and, we need to maintain this chain until we reach the page life cycle method which originally got called. This doesn't look comfortable.
  • It opens a risk of not writing the return statement after the base.Redirect() method at times, and this will result in execution of all other codes which are written after the base.Redirect() statement.

To me, this is the biggest problem.

If you forget to write the return statement after calling the base.Redirect() method (Which is a very easy mistake to do), disaster might happen. How would you deal with this risk? Unfortunately, I do not see any way!

Enough! Tell me what to do 

OK, I know I danced around the original problem for long enough, and if you have patience to read up to this far, considering all the facts, let me tell you now how I think the Response.Redirect() should be handled in the best possible way:

  • DO NOT WRITE any try..catch block in the CodeBehind. This will make sure you are not catching any ThreadAbortException there.
  • Redirect using the classic Response.Redirect(url) statement as you love to do :)
  • Simply ignore the ThreadAbortException in the Global.asax (Where you usually catch your un-handled exceptions and report), using the following code:
void Application_Error(object sender, EventArgs e)
{ 
        // Code that runs when an un-handled error occurs
        Exception ex = Server.GetLastError();
        
        System.Threading.ThreadAbortException exception = ex as System.Threading.ThreadAbortException;
        if (exception != null)
        {
             //Log or report only if this is not ThreadAbortException
              Logger.LogException(exception );   
        }
} 

Or, another alternative approach may simply be to swallow the ThreadAbortException in the BasePages Redirect() method and call that method everywhere where there is a need to redirect.

protected void Redirect(string url)
{
        try
        {
            Response.Redirect(url);
        }
        catch (ThreadAbortException ex)
        {
            //Swallow it!
        }
}
base.Redirect(); //Redirects to a different URL

Yes, I know some of you might scream and shout at me "What the hell you are saying? You are letting an Exception to be thrown, and, just eating it after catching". You know exceptions are heavy and you shouldn't do this and that...

Let me tell you, this is what I learned from my exploration on ThreadAbortException and Response.Redirect(). Suggest me a better way if you can! 

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"