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:
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 ;)
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:
Here I must add two notes about best practice of Exception handling in my opinion:
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:
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.
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.