ASP.NET Error Handling Using HttpModule, Full and Partial Post Back (Ajax UpdatePanel)
This post is the reason for me opening a blog. Recently I encountered a very simple problem that took me around 3 days to solve. I found many posts on this issue but nothing was what I needed.
My task was quite simple I wanted a single point of logic to deal with the fatal errors:
- When an error occurs that is not a result of invalid input by the user - and we cannot recover from it.
- exception we didn't or couldn't catch (unhandled exceptions).
I must say that most of my experience is with server side so my knowledge in ASP.NET is quite poor (Many thanks to Avi Pinto for all his help ;)
The error handling flow is:
- Log the Exception
- Open a generic error page with the error code number and more info.
In ASP.NET there are two ways of doing this. The first is using the Global.asax file ( which I will not discuss here) and the other is using HttpModule ( a detailed explanation about HttpModules is beyond the scope of this article. You can read about it here).
I choose the HttpModule because it's more reusable - if it needs to be altered you don't need to rebuild your whole web site, just add a reference DLL of the class that implements the interface IHttpModule and add to the web.config file one line:
type="MyHttpModule, MyHttpModule" />
Implementing the IHttpModule:
- In the Init method enroll to the context error event.
- Implementing the error handler (my logic): just write to log, build query string for the error page and then Server.Transfer to the error page with the query string generated.
Here I must add two notes about best practice of Exception handling in my opinion:
Note 1: Some developers tend to use the exception technology for logic implementation (e.g. throw DataNotFoundException for empty data). I think it's bad practice, just return empty result. The Exception technology has very high overhead and wasn't design for logic implementation.
Note 2: in your lower levels of logic ( e.g. code behind of a page ) don't catch Exception or WebException unless you have something to do with them: the only reason to catch general Excption is maybe to add information and then rethrow it.
I tested the HttpModule just by removing the catch(Exception ex) lines from the solution I was working on.
It appeared to be working :-) but trying it on another web page it crashed. I checked why and discovered that the error was thrown from ajax UpdatePanel control. Looking for an answer on the web I found that many people encountered this problem - when you modify the response during a partial post back the response is turned into gibberish). Some solutions allowed redirect from an ajax update panel but not inside a HttpModule which is called during the request life cycle.
First I noticed that in case of a partial post back my module doesn't catch the error. I found the solution to that on the web:
- enroll to the event PostMapRequestHandler in the module's Init method (there is a parallel way with the Global.asax)
public void Init(HttpApplication context)
{ context.Error += new EventHandler(context_Error);
context.PostMapRequestHandler += new EventHandler(context_PostMapRequestHandler);
}
- In the PostMapRequest event handler cast the handler to a Page object and enroll to the Page.Error event
void context_PostMapRequestHandler(object sender, EventArgs e)
Page aux = HttpContext.Current.Handler as Page;
aux.Error += new EventHandler(aux_Error);
This way we catch all the errors (partial and full post back). This allowed me to log the error but the Server.Transfer (and Response.Redirect) was not working :-( - again, because you cannot change the request in partial post back. This sent me back to the web for more information. I found one answer that suggested I add the module to the web.config file:
<add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
This will enable the use of Response.Redirect, but.....not from a HttpModule. In one of my tests I entered the wrong error page URL and then got a PageNotFound Error (404). When I checked, i found that the error was back in a full post back. From here the way to the solution was near.
The solution (pay attention it's tricky):
- I am enrolling to both errors of full post back and errors of all types (full and partial post back).
- In the error handler of all typed post back: log my error and redirect to URL that doesn't exist.
- In the error handler of full post back: check if the error was as a result of my page not found error (404) or not. If not just clear the error otherwise Server.Transfer to the error page.
the code:
public class ClientErrorHandlingModule : IHttpModule
{ public const string USER_ID_QS = "UserId";
public const string ERROR_CODE_QS = "ErrorCode";
public const string DATE_TIME_QS = "DateTime";
public const string SOURCE_PAGE_URL_QS = "SorucePageUrl";
public const string ADMIN_MAIL_ADD_QS = "AdminMailAdd";
private const string ERROR_PAGE_URL = "Error.aspx";
private const string NO_PAGE_STR = "No+Page.aspx";
public ClientErrorHandlingModule()
{ //
// TODO: Add constructor logic here
//
}
#region IHttpModule Members
public void Dispose()
{ }
public void Init(HttpApplication context)
{ context.Error += new EventHandler(context_Error);
context.PostMapRequestHandler +=
new EventHandler(context_PostMapRequestHandler);
}
void context_PostMapRequestHandler(object sender, EventArgs e)
{ Page aux = HttpContext.Current.Handler as Page;
if (aux != null)
{ aux.Error += new EventHandler(aux_Error);
}
}
//all error full and partial post back handler
void aux_Error(object sender, EventArgs e)
{ string srcPageUrl =
HttpContext.Current.Request.Url.ToString();
Exception ex = HttpContext.Current.Server.GetLastError();
int userId = retrieveUserId();
//log the error
ExceptionMgr.HandleClientException(ex,
EventIDTypes.GeneralWebError, userId, srcPageUrl);
//retrieve the error page query string
string errorPageQueryStr = getErrorPageQueryStr
(srcPageUrl, ex);
//redirect to no place in order to generate post back
// normal error
HttpContext.Current.Response.Redirect(NO_PAGE_STR + "?"
+ errorPageQueryStr, false);
}
//only full postback handler
void context_Error(object sender, EventArgs e)
{ try
{ string errorPageQueryStr = string.Empty;
string srcPageUrl =
HttpContext.Current.Request.Url.ToString();
//if the error comes our handler we retrieve the
//query string
if (srcPageUrl.Contains(NO_PAGE_STR))
{
errorPageQueryStr = srcPageUrl.Substring
(srcPageUrl.IndexOf(NO_PAGE_STR)
+ NO_PAGE_STR.Length + 1);
}
//otherwise we just clear the error
else
{ HttpContext.Current.Server.ClearError();
return;
}
//clear the error and redirect it to the error page
HttpContext.Current.Server.ClearError();
HttpContext.Current.Server.Transfer(ERROR_PAGE_URL +
"?" + errorPageQueryStr, true);
}
catch (Exception ex)
{ LogMgr.Write(LogCategories.Fatal,
"context_Error exception:"
+ ex.Message,EventIDTypes.GeneralInfraError, ex);
}
}
private string getErrorPageQueryStr(string i_srcPageUrl,
Exception i_ex)
{ ClientErrorConfigurationSection confSec = ConfigMgr.Read
(ConfigSectionTypes.ClientError) as
ClientErrorConfigurationSection;
if (confSec == null)
throw new Exception("Problem in reading Client Error config file");
SoapExceptionInfo sei = getSoapExceptionInfo(i_ex);
string errorPageQueryStr = getErrorPageQueryString(sei,
(int)EventIDTypes.GeneralWebError,
confSec.AdminMailAddress, i_srcPageUrl);
return errorPageQueryStr;
}
private string getErrorPageQueryString(SoapExceptionInfo i_sei,
int i_errorCode, string i_adminMailAddress,
string i_srcPageUrl)
{ StringBuilder sb = new StringBuilder();
string template = "{0}={1}&"; sb.Append(string.Format(template, DATE_TIME_QS,
DateTime.Now));
sb.Append(string.Format(template, ADMIN_MAIL_ADD_QS,
i_adminMailAddress));
sb.Append(string.Format(template, SOURCE_PAGE_URL_QS,
i_srcPageUrl));
if (i_sei != null)
{ sb.Append(string.Format(template, ERROR_CODE_QS,
i_sei.ErrorCode));
}
else
{
sb.Append(string.Format(template, ERROR_CODE_QS,
i_errorCode));
}
return sb.ToString();
}
private SoapExceptionInfo getSoapExceptionInfo(Exception i_ex)
{ SoapExceptionInfo res = null;
if (i_ex is SoapException)
{ res = SoapExceptionHandler.TranslateSoapException(
(SoapException)i_ex);
}
return res;
}
private static int retrieveUserId()
{ int userId = -1;
string userIdStr =
HttpContext.Current.Request.Params[USER_ID_QS];
if (!string.IsNullOrEmpty(userIdStr))
{ userId = int.Parse(userIdStr);
}
return userId;
}
#endregion
}
To conclude, the goal was reached: only one place holding the logic for error handling. It works not only with ajax updatepanel but also for all regular post backs.
So that's all for now I hope this post will save others some time.
Cheers Offir