0121 31 45 374
Qoute Icon

Reporting errors from AJAX using the WebException Class

Tim

I’ve been using Phil Whinstanley’s error reporting class1 within my applications for some time now and it really does help with diagnosing issues with the site’s during development (or client testing) but also alerting me to errors on live sites. I also like it because it can highlight hacking attempts and also spambot form submissions –allowing you to alter the site as needed. A lot of the time it also means we’re alerted to an issue with the site before the client has a chance to call.

1 Note: I've been told the files Phil put online all those years ago are offline but don't panic, I'm posting another post with the relevant files shortly. If you don't want to use the search function (top right) or you're just keen, check out my comment within my post about ASP.Net WebException and Error Reporting useful code.

I’m glad he developed it because before this was around I was using a very simple email alert system that didn’t contain even a third of what this one does. Historically in ASP we always reported 500-100 errors as I don’t like clients spotting issues before I do. It’s very important to include error reporting in your code otherwise you may miss a sequence of events that causes your client to loose out on a sale.

Recently however we got in on the Atlas/AJAX scene pretty early on because we had a new application that would really benefit from a lack of postback and as it was an internal application only where we had complete control over the user’s environment, accessibility wasn’t so much of a concern (though FWIW you can still use the site in the same way without JavaScript activated).

At present, our development server’s SMTP server isn’t working properly so I didn’t think anything of receiving no email when I threw an exception during the early stages of development but as soon as I threw it onto the live server I quickly noticed that I wasn’t receiving errors from the application (we’ve got a test page to ensure the error reporting is working as expected), on investigation I found that the errors were being caught by the Atlas/AJAX handler (in a similar way to a try/catch block) which meant no emails were being sent out –so what do you do?

Note: Since I first started this article, Atlas has been released by Microsoft and is now AJAX and as part of the current release, Atlas/AJAX allows you to capture errors that are otherwise trapped by the framework and handle them as you like but for completeness I’ll overview things I tried.

Firstly I tried simply bubbling the error up to the global.asax’s Application_Error event handler as I normally would but that won’t work as it will still be trapped by the Atlas/AJAX framework, further more, the error returned to the user isn’t very useful (it’s the text within the exception):

Example standard Atlas/AJAX error - a pretty useless error message as far as the user is concerned!

The next thing I tried was taking the exception and passing it to the WebException as you do within the Application_Error event handler, although this worked and for this project would have been an alright solution because the ScriptManager was contained within a single MasterPage, I wanted a solution that I could easily roll out to other projects.

What I decided to do in the end was to wrap the WebException class and adding a single static method that takes an exception, then I replaced the code within the Global.asax and within the ScriptManager’s error event handler and responded to the user with a more informative message. The code below will output a user friendly message -still in a popup though you could redirect if desired. In the live application the user's location and a reference for the incoming error email is also shown to the user.

Note: TSDGlobals is a settings class we use here, it just references the relevant setting and contains a set of useful methods that we use throughout most of our projects.

aspx code

<asp:ScriptManager runat="server" ID="sm" EnablePartialRendering="true" AllowCustomErrorsRedirect="true" OnAsyncPostBackError="atlasScriptManager_PageError"></asp:ScriptManager>

codebehind

protected void atlasScriptManager_PageError(object sender, AsyncPostBackErrorEventArgs e)
{
    //A page reference for you (optional but useful)
    string __PageRef = "132";
    //Update the message the user will see
    sm.AsyncPostBackErrorMessage = String.Format("I'm sorry,  an error has occured, please contact us on 01234 567890. Quoting Page Ref: {0} - {1}", __PageRef, DateTime.Now.ToString());
    //Pass it through to the new Error Handler
    ErrorHandling.ErrorHandler.Handle(e.Exception);
}

global.asax

void Application_Error(object sender, EventArgs e)
{
    ErrorHandling.ErrorHandler.Handle(Server.GetLastError());
}

protected void Application_PreRequestHandlerExecute(Object sender, EventArgs e)
{
    if (Context.Handler is IRequiresSessionState || Context.Handler is IReadOnlySessionState)
        ErrorReporting.SessionTracker.AddRequest("Pre Request Handler Execute"truetruefalse);
}

ErrorHandler.cs

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

namespace ErrorHandling
{
    public class ErrorHandler
    {
        //Declare for the scope of the class
        private static HttpRequest context = HttpContext.Current.Request;

        public static void Handle(Exception currentError)
        {
            Handle(currentError, true);
        }

        public static void Handle(Exception currentError, bool redirectUser)
        {
            if (TSDGlobals.SendSiteErrors)
            {
                #region Deal with 404's

                //Redirect the user to a friendly page
                if (CheckForErrorType(currentError, "FileNotFound") && redirectUser)
                    RedirectToFriendlyUrl(TSDGlobals.ErrorPage_PageNotFound);

                #endregion
                #region Deal with Spambots

                //Check the error type
                if (CheckForErrorType(currentError, "System.FormatException"))
                {
                    if (context.Form.Count > 0)
                    {
                        foreach (string key in context.Form)
                        {
                            if (key.IndexOf("_VIEWSTATE") > 0 && context.Form[key].ToString().IndexOf("Content-Type") > 0)
                                return;
                        }
                    }
                }

                #endregion

                //Enable the trace for the duration of the error handling
                TraceContext t = HttpContext.Current.Trace;
                bool bCurrentState = t.IsEnabled;
                t.IsEnabled = true;

                #region Handle the Exception

                WebException WE = new WebException();
                WE.CurrentException = currentError;
                WE.Site = context.Url.Host.ToString();
                //Pull the information from the web.config here if desired
                WE.FloodCount = 50;
                WE.FloodMins = 5;

                #endregion
                #region Choose what you're interested in

                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

                WE.Handle();

                //Return the trace to its original state
                t.IsEnabled = bCurrentState;

                //Redirect the user to a friendly page
                if (redirectUser)
                    RedirectToFriendlyUrl(TSDGlobals.ErrorPage_CodeIssue);
            }
        }

        private static bool CheckForErrorType(Exception ex, string errorText)
        {
            if (ex != null)
            {
                //Check the exception
                if (ex.GetType().ToString().IndexOf(errorText) > 0)
                    return true;
                else
                    return CheckForErrorType(ex.InnerException, errorText);
            }
            else
            {
                return false;
            }
        }

        private static void RedirectToFriendlyUrl(string Url)
        {
            //Only redirect the user if the URL is not empty and we're not on a dev machine
            //TODO: Check the referrer to ensure we don't redirect the user to the page causing the error!
            //TODO: Pull the list of development server addresses from an XML file
            if (!String.IsNullOrEmpty(Url) && (context.Url.Host.IndexOf("localhost") < 0))
                HttpContext.Current.Response.Redirect(Url);
        }
    }
}

I’m not sure if this is a recommended way of doing it but it works pretty well and in my case, the majority of settings from the code are the same regardless of the project but you can still alter those if required –as they’re not likely to change project-project I’ve kept the settings within the web.config. I decided to wrap Phil’s code in my own because that way if he ever releases an update (not sure what that’d do tbh) I could just drop the new WebException code into my project and be ready to go straight away.

What do you think Phil? Use or Abuse of your code ;)

Liked this post? Got a suggestion? Leave a comment