by John S. Reid
March 31, 2004
(edited October 28, 2006 to include greater detail and fix some inaccuracies
in my analysis, though the solution at it's core remains the same)
Since the advent of Server.Transfer(url) I've used Response.Redirect(url) less and
less, but for those times when you want the client to know of the location from
where information will be retrieved it's still your best option. So you use it,
never expecting that the simple call you've used for years with ASP would throw
an exception in ASP.NET.
The ThreadAbortException is thrown when you make a call to Response.Redirect(url)
because the system aborts processing of the current web page thread after it sends
the redirect to the response stream. Response.Redirect(url) actually makes a
call to Response.End() internally, and it's Response.End() that calls Thread.Abort()
which bubbles up the stack to end the thread. Under rare circumstances the call to
Response.End() actually doesn't call Thread.Abort(), but instead calls HttpApplication.CompleteRequest().
(See this
Microsoft Support article for details and a hint at the solution.)
Though one could argue the logic behind throwing this exception in the first place,
there are a couple of ways to get around this.
The try-catch block
try
{
Response.Redirect(url);
}
catch (ThreadAbortException ex)
{
}
The first solution, and the most common one found in an internet search, is to simply
wrap the call to Response.Redirect in a try-catch block. Although it appears to
work I don't like it for two reasons:
- Processing an exception can be costly and messy
- The rest of the page and event chain continues to execute
Since the exception is caught the page continues to execute through it's event chain
which takes up unnecessary CPU resources and potentially sends unwanted data to
the client (which will be ignored since the client received a redirect).
A better way.
Since the real culprit is the call to Thread.Abort(), how can we Redirect
the page without making that call? We would have to tell the client to redirect
without calling Response.End() of course, and it turns out that there is an overload
method of Response.Redirect() that will do exactly that.
Response.Redirect(string url,
bool endResponse);
In this overload the second parameter tells the system whether to make the internal
call to Response.End() or not. When this parameter is false the client is sent the
redirect url, but the internal call to Response.End is skipped. This completely
avoids the code that would throw the exception, but the cost is that this thread
doesn't stop executing the Application events! Thankfully we can solve that problem
also by duplicating the step that Response.End() takes under those rare circumstances,
namely calling the HttpApplication.CompleteRequest() method.
HttpApplication.CompleteRequest() sets a variable that causes the thread to skip
past most of the events in the HttpApplication event pipeline and go straight to the
final event, named HttpApplication.EventEndRequest. This gracefully ends execution of the
thread with a minumum of server resources.
Alain Renon pointed out to me on Gabriel Lozano-Morán's blog
that the page still sends the rendered HTML to the client. He's absolutely right. In fact,
Page events still execute and the Page still processes postback events too. The event
pipeline that I am talking about short circuiting with the call to HttpApplication.CompleteRequest()
is not the Page event chain but the Application event chain. The Application event chain
currently consists of the following events as of .NET 2.0:
ValidatePathExecutionStep
UrlMappingsExecutionStep
EventBeginRequest
EventAuthenticateRequest
EventDefaultAuthentication
EventPostAuthenticateRequest
EventAuthorizeRequest
EventPostAuthorizeRequest
EventResolveRequestCache
EventPostResolveRequestCache
MapHandlerExecutionStep
EventPostMapRequestHandler
EventAcquireRequestState
EventPostAcquireRequestState
EventPreRequestHandlerExecute
CallHandlerExecutionStep
EventPostRequestHandlerExecute
EventReleaseRequestState
EventPostReleaseRequestState
CallFilterExecutionStep
EventUpdateRequestCache
EventPostUpdateRequestCache
EventEndRequest
I've bolded CallHandlerExecutionStep because that is the step where all of the
Page events take place. If you make a call to HttpApplication.CompleteRequest()
from a Page event such as Page_Load then the Page events still process to
completion, but all the Application events between CallHandlerExecutionStep
and EventEndRequest are skipped and the thread ends gracefully.
One could argue that it doesn't matter so much that the page is still rendered since
the client ignores it anyway, but Alain is correct that it does render, and that's
not as efficient as we could make it. Also, if you have any PostBack processing
(such as OnCommand Page event handlers) those will process and you may not want that
either, but never fear there is an easy solution to both of those problems as well.
PostBack and Render Solutions? Overrides.
The idea is to create a class level variable that flags if the Page should terminate
and then check the variable prior to processing your events or rendering your page.
This flag should be set after the call to HttpApplication.CompleteRequest().
You can place the check for this value in every PostBack event or rendering block
but that can be tedious and prone to errors, so I would recommend just overriding
the RaisePostBackEvent and Render methods as in the code sample below:
private bool m_bIsTerminating =
false;
protected void Page_Load(
object sender,
EventArgs e)
{
if (WeNeedToRedirect ==
true)
{
Response.Redirect(url, false);
HttpContext.Current.ApplicationInstance.CompleteRequest();
m_bIsTerminating = true;
// remember to end the method here if
// there is more code in it
return;
}
}
protected override void RaisePostBackEvent(IPostBackEventHandler sourceControl,
string eventArgument)
{
if (m_bIsTerminating ==
false)
base.RaisePostBackEvent(sourceControl, eventArgument);
}
protected override void Render(HtmlTextWriter writer)
{
if (m_bIsTerminating ==
false)
base.Render(writer);
}
The Final Analysis
Initially I had recommended that you should simply replace all of your calls to
Response.Redirect(url) with the Response.Redirect(url, false) and CompleteRequest()
calls, but if you want to avoid postback processing and html rendering you'll
need to add the overrides as well. From my recent in depth analysis of the code I
can see that the most efficient way to redirect and end processing is to use the
Response.Redirect(url) method and let the thread be aborted all the way up the
stack, but if this exception is causing you grief as it does in many circumstances
then the solution here is the next best thing.
It should also be noted that the Server.Transfer() method suffers from the same
issue since it calls Response.End() internally. The good news is that it can be solved
in the same way by using the solution above and replacing the call to
Response.Redirect() with Server.Execute().
Happy coding!