ASP.Net WebException and Error Reporting useful code
I’ve been including Phil Winstanley’s error reporting code (http://weblogs.asp.net/plip/archive/2004/04/17/114984.aspx) in our projects for quite some time now and it’s become an invaluable resource when debugging site exceptions both while developing and on live sites.
Since the initial release Phil’s class, we’ve been expanding it in various ways to suit so I thought in addition to the trace output work around that I posted about a while ago I thought I would share some other techniques we use.
Quickly turning it off while developing
I have seen people writing checks to ensure the site isn’t local before reporting the error but rather than hard coding that we’ve added a value to the web.config “SendErrors” it it’s true then the error report is sent.
//Aflagintheweb.configfiletoallowyoutodisableerrorreportingwhiletesting(ifyouwantto!)bool sendErrors = Convert.ToBoolean(ConfigurationManager.AppSettings["SendSiteErrors"]);if (sendErrors){...}{//HandletheError}
Check what sort of error it is
There’s no point in throwing an exception error if the error message is a 404 so check the error type and redirect the user to a specific error page for the given error page, i.e. for a 404 you could display the URL they attempted to visit explain what may have happened. How about doing a little selling to them while they’re there? Or as some companies have started doing, offer them a discount for the inconvienience!
//Redirecttheusertoafriendlypageif (CheckForErrorType(currentError, "FileNotFound")) RedirectToFriendlyUrl("Your404ErrorPage.htm");///<summary>///Helpermethodtocheckwhatsortoferrorwe'redealingwith///</summary>///<param name="ex">Exceptiontocheck</param>///<param name="errorText">Stringvaluetofind</param>///<returns>trueiffound,falseifnot</returns>privatestaticbool CheckForErrorType(Exception ex, string errorText){...}{if (ex != null){...} {//Checktheexception if it's not found in the outer ex, check it's children if (ex.GetType().ToString().IndexOf(errorText) > 0)returntrue;elsereturn CheckForErrorType(ex.InnerException, errorText); }else{...} {returnfalse; }}///<summary>///Redirectstheusertoagivenurlintheeventofanerror///</summary>///<param name="Url">URLofthepagetoredirectto</param>privatestaticvoid RedirectToFriendlyUrl(string Url){...}{//OnlyredirecttheuseriftheURLisnotemptyandwe'renotonadevmachine//TODO:Checkthereferrertoensurewedon'tredirecttheusertothepagecausingtheerror!if (!String.IsNullOrEmpty(Url) && (context.Url.Host.IndexOf("localhost") < 0))HttpContext.Current.Response.Redirect(Url);}
Make it more generalised
In Phil’s class he allows you to set the site name for the error report, we generally just wrote the domain name in for simplicity, so instead, just use: Request.Url.Host
WE.Site = context.Url.Host;
Rid yourself of Spambot attacks
Sean Ronan from Activepixels was showing me his method of getting around the spambot attacks that are becoming a daily annoyance once the method was implemented it cut our dud error reports from up to 400 per site per night to around 0 :)
Sean noticed that although the spambots were getting clever and identifying ASP.Net forms to post back to, they were also altering the __VIEWSTATE field which caused a System.FormatException error to be thrown.
If you’re also using Phil Winstanley's ASP.Net WebException handler you’ll no doubt have been inundated with error reports, the main issue imho is the flood count, if you were getting 50 form posts, you’re likely to miss the important one.
The solution is remarkably simple: if the error message is a System.FormatException then simply check if the one of the form fields is “__VIEWSTATE” and has “Content-Type” as part of it’s content, if it does then don’t report it –easy!
//Checktheerrortypeif (CheckForErrorType(currentError, "System.FormatException")){...}{//Ensurethere'saformcollectiontoparseif (context.Form.Count > 0){...} {//Loopthrougheachitemintheformcollectiontocheckfor_VIEWSTATEforeach (string key in context.Form){...} {//Ifthere'saviewstatematchandwithinit"Content-Type"(usedbythespambots)thenreturnif (key.IndexOf("_VIEWSTATE") > 0 && context.Form[key].ToString().IndexOf("Content-Type") > 0)return; } }}
An example of the “spambotted” ViewState courtesy of Infobrokers:
__VIEWSTATE awtxhf@infobrokers.co.uk Content-Type: multipart/mixed; boundary="===============0240301364==" MIME-Version: 1.0 Subject: 342f9a6 To: awtxhf@infobrokers.co.uk bcc: homeigoldstein@aol.com From: awtxhf@infobrokers.co.uk This is a multi-part message in MIME format. --===============0240301364== Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit oos --===============0240301364==--
Persisting the code across projects
There are perhaps better ways of persisting the code across projecst but we’ve copied the various code snippets for the Global.asax and Web.Config file onto the Visual Studio Toolbox offering us drag and drop setup.
One better alternative would be to add it to the Global.asax template, you can find it at:%ProgramDir%\Microsoft Visual Studio 8\Common7\IDE\ItemTemplatesCache\Web\CSharp\1033\GlobalAsax.zip
Just paste your code in there and bobs your uncle. I’ve got a feeling you can write your own project templates too if you generate that many projects.
Eventually I expect I’ll wrap Phil’s WebException with our own generic handler that does all this for us but for now I’m happy with this method.
Enjoy.
The relevant code in its entirety:
//DeclareforthescopeoftheclassprivatestaticHttpRequest context = HttpContext.Current.Request;void Application_Error(object sender, EventArgs e){...}{//Aflagintheweb.configfiletoallowyoutodisableerrorreportingwhiletesting(ifyouwantto!)bool sendErrors = Convert.ToBoolean(ConfigurationManager.AppSettings["SendSiteErrors"]);if (sendErrors){...} {Deal with 404's#region Deal with 404's//Redirecttheusertoafriendlypageif (CheckForErrorType(currentError, "FileNotFound")) RedirectToFriendlyUrl("Your404ErrorPage.htm");#endregionDeal with Spambots#region Deal with Spambots//Checktheerrortypeif (CheckForErrorType(currentError, "System.FormatException")){...} {//Ensurethere'saformcollectiontoparseif (context.Form.Count > 0){...} {//Loopthrougheachitemintheformcollectiontocheckfor_VIEWSTATEforeach (string key in context.Form){...} {//Ifthere'saviewstatematchandwithinit"Content-Type"(usedbythespambots)thenreturnif (key.IndexOf("_VIEWSTATE") > 0 && context.Form[key].ToString().IndexOf("Content-Type") > 0)return; } } }#endregion//Enablethetraceforthedurationoftheerrorhandling//Note:Seepreviousblogpostaboutthisnotworkinginthelatest//versionofthereportingcode(attimeofwritingv4)TraceContext t = HttpContext.Current.Trace;bool bCurrentState = t.IsEnabled; t.IsEnabled = true;Handle the Exception#region Handle the Exception ErrorHandling.WebException WE = new ErrorHandling.WebException(); WE.CurrentException = currentError; WE.MailFrom = "you@yourdomain.co.uk"; WE.MailTo = "you@yourdomain.co.uk"; WE.MailAdmin = "you@yourdomain.co.uk"; WE.Site = context.Url.Host; WE.SmtpServer = "localhost"; WE.FloodCount = 10; WE.FloodMins = 5;#endregionChoose what you're interested in#region Choose what you'reinterestedin WE.ReturnCache = true; WE.DrillDownInCache = true; WE.IncludeApplication = true; WE.IncludeBrowser = true; WE.IncludeEnvironmentVariables = true; WE.IncludeForm = true; WE.IncludeProcess = true; WE.IncludeQueryString = true; WE.IncludeRequestCookies = true; WE.IncludeRequestHeader = true; WE.IncludeResponseCookies = true; WE.IncludeServerVariables = true; WE.IncludeSession = true; WE.IncludeTrace = true; WE.IncludeVersions = true; WE.IncludeAuthentication = true;#endregion//Sendofftheerrorreport WE.Handle();//Returnthetracetoitsoriginalstate t.IsEnabled = bCurrentState;//Redirecttheusertoafriendlypage RedirectToFriendlyUrl("Your500ErrorPageHere.htm"); }}protectedvoid Application_PreRequestHandlerExecute(Object sender, EventArgs e){...}{//WebResource.axddoesn'timplementIRequireSessionStatesowillthrowanerrorwithoutthischeckif (Context.Handler isIRequiresSessionState || Context.Handler isIReadOnlySessionState) ErrorReporting.SessionTracker.AddRequest("PreRequestHandlerExecute", true, true, false);}///<summary>///Helpermethodtocheckwhatsortoferrorwe'redealingwith///</summary>///<param name="ex">Exceptiontocheck</param>///<param name="errorText">Stringvaluetofind</param>///<returns>trueiffound,falseifnot</returns>privatestaticbool CheckForErrorType(Exception ex, string errorText){...}{if (ex != null){...} {//Checktheexception if it's not found in the outer ex, check it's childrenif (ex.GetType().ToString().IndexOf(errorText) > 0)returntrue;elsereturn CheckForErrorType(ex.InnerException, errorText); }else{...} {returnfalse; }}///<summary>///Redirectstheusertoagivenurlintheeventofanerror///</summary>///<param name="Url">URLofthepagetoredirectto</param>privatestaticvoid RedirectToFriendlyUrl(string Url){...}{//OnlyredirecttheuseriftheURLisnotemptyandwe'renotonadevmachine//TODO:Checkthereferrertoensurewedon'tredirecttheusertothepagecausingtheerror!if (!String.IsNullOrEmpty(Url) && (context.Url.Host.IndexOf("localhost") < 0))HttpContext.Current.Response.Redirect(Url);}
Finally: Since playing with Atlas/AJAX I've found that the errors are caught by the Atlas engine, I've worked out a way around it and will post about it later.