views:

855

answers:

2

I'm having an issue with custom errors on an ASP.NET MVC app I've deployed on Go Daddy. I've created an ErrorController and added the following code to Global.asax to catch unhandled exceptions, log them, and then transfer control to the ErrorController to display custom errors. This code is taken from here:

    protected void Application_Error(object sender, EventArgs e)
    {
        Exception ex = Server.GetLastError();
        Response.Clear();

        HttpException httpEx = ex as HttpException;
        RouteData routeData = new RouteData();
        routeData.Values.Add("controller", "Error");

        if (httpEx == null)
        {
            routeData.Values.Add("action", "Index");
        }
        else
        {
            switch (httpEx.GetHttpCode())
            {
                case 404:
                    routeData.Values.Add("action", "HttpError404");
                    break;
                case 500:
                    routeData.Values.Add("action", "HttpError500");
                    break;
                case 503:
                    routeData.Values.Add("action", "HttpError503");
                    break;
                default:
                    routeData.Values.Add("action", "Index");
                    break;
            }
        }

        ExceptionLogger.LogException(ex); // <- This is working. Errors get logged

        routeData.Values.Add("error", ex);
        Server.ClearError();
        IController controller = new ErrorController();
        // The next line doesn't seem to be working
        controller.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
    }

Application_Error is definitely firing because the logging works fine, but instead of displaying my custom error pages, I get the Go Daddy generic ones. From the title of the blog post the above code is taken from, I notice that it uses Release Candidate 2 of the MVC framework. Did something change in 1.0 that makes the last line of code not work? As usual it works great on my machine.

Any suggestions will be greatly appreciated.

Edit: Forgot to mention that I've tried all 3 possiblities for the customErrors mode in Web.config (Off, On, and RemoteOnly). Same results regardless of this setting.

Edit 2: And I've also tried it with and without the [HandleError] decoration on the Controller classes.

Update: I've figured out and fixed the 404s. There is a section of the Settings panel in Go Daddy's Hosting Control Center where 404 behavior can be controlled and the default is to show their generic page, and apparently this overrides any Web.config settings. So my custom 404 page is now showing as intended. However, 500s and 503s are still not working. I've got code in the HomeController to grab a static text version of the content if Sql Server throws an exception as follows:

    public ActionResult Index()
    {
        CcmDataClassesDataContext dc = new CcmDataClassesDataContext();

        // This might generate an exception which will be handled in the OnException override
        HomeContent hc = dc.HomeContents.GetCurrentContent();

        ViewData["bodyId"] = "home";
        return View(hc);
    }

    protected override void OnException(ExceptionContext filterContext)
    {
        // Only concerned here with SqlExceptions so an HTTP 503 message can
        // be displayed in the Home View. All others will bubble up to the
        // Global.asax.cs and be handled/logged there.
        System.Data.SqlClient.SqlException sqlEx =
            filterContext.Exception as System.Data.SqlClient.SqlException;
        if (sqlEx != null)
        {
            try
            {
                ExceptionLogger.LogException(sqlEx);
            }
            catch
            {
                // couldn't log exception, continue without crashing
            }

            ViewData["bodyId"] = "home";
            filterContext.ExceptionHandled = true;
            HomeContent hc = ContentHelper.GetStaticContent();
            if (hc == null)
            {
                // Couldn't get static content. Display friendly message on Home View.
                Response.StatusCode = 503;
                this.View("ContentError").ExecuteResult(this.ControllerContext);
            }
            else
            {
                // Pass the static content to the regular Home View
                this.View("Index", hc).ExecuteResult(this.ControllerContext);
            }
        }
    }

Here's the code that attempts to fetch the static content:

    public static HomeContent GetStaticContent()
    {
        HomeContent hc;

        try
        {
            string path = Configuration.CcmConfigSection.Config.Content.PathToStaticContent;
            string fileText = File.ReadAllText(path);
            string regex = @"^[^#]([^\r\n]*)";
            MatchCollection matches = Regex.Matches(fileText, regex, RegexOptions.Multiline);
            hc = new HomeContent
                {
                    ID = Convert.ToInt32(matches[0].Value),
                    Title = matches[1].Value,
                    DateAdded = DateTime.Parse(matches[2].Value),
                    Body = matches[3].Value,
                    IsCurrent = true
                };
        }
        catch (Exception ex)
        {
            try
            {
                ExceptionLogger.LogException(ex);
            }
            catch
            {
                // couldn't log exception, continue without crashing
            }
            hc = null;
        }

        return hc;
    }

I've verified that if I change the connection string to generate a SqlException, the code properly logs the error and then grabs and displays the static content. But if I also change the path to the static text file in Web.config to test the 503 version of the Home View, what I get instead is a page with nothing other than "service unavailable". That's it. No custom 503 message with the look and feel of the site.

Does anyone have any suggestions on improvements to the code that might help? Would it help to add different headers to the HttpResponse? Or is Go Daddy heavy-handedly hijacking the 503s?

A: 

I host an ASP.NET MVC site on GoDaddy and also faced issues dealing with custom error pages. What I found, through trial and error, was that GoDaddy intercepts errors at the HTTP level.

For example, any page which returned an HTTP status code of 404 caused GoDaddy's custom error page to take over. Eventually I changed my custom error pages to return 200 status and the 404-related problem went away. My HTML was the same, just the HTTP status needed to change.

I admittedly never tried doing the same with 503 status responses, but it's possible that the same mitigation may work. If you change from returning a 503 status to returning 200 status, does the problem go away?

Note that, if you do this workaround, you'll want to prevent search engines from indexing your error pages, which once then return a 200 status will be indistinguishable (from the search engine's perspective) from a regular page. So make sure to add a META ROBOTS tag to prevent indexing of your error pages, e.g.

<META NAME="ROBOTS" CONTENT="NOINDEX">

The downside of this approach may be that your page might be removed from Google, which is definitely not a good thing!

UPDATE: So, in addition, you can also detect whether the user agent is a crawler or not, and if it's a crawler return a 503 while if it's not a crawler, return a 200. See this blog post for info about how to detect crawlers. Yes, I know that returning different content to crawlers vs. users is an SEO no-no, but I've done this on several sites with no ill effect so far, so I'm not sure how much of a problem that is.

Doing both approaches (META ROBOTS and bot detection) may be your best bet, in case any oddball crawlers slip through the bot detector.

Justin Grant
Thanks Justin. I finally figured out how to fix the 404s, see my update above. The problem with this approach in my situation is that some of the 503s are to display the friendly message on the actual page where the content would have been. If I return a 200 instead of 503 and am unlucky enough to have any crawlers indexing that day, they will index my friendly error message instead of the correct content. So this won't work in my particular case. Thanks for the reply though.
Bryan
yep, gotta watch out for crawlers. I noted in my original post that you can use a ROBOTS META tag to get around this, but I just added more details too. See above.
Justin Grant
You may not have seen any ill effects, but there's no way to know that the same would hold true for me and my client :-). I'm hesitant to consider your approach since it could result in my client, and by extension me, being removed and/or banned from search engine indexes.
Bryan
I don't think this is a problem in this case. Here's why: errors should be unusual and short-lived, or your client will fire you before Google does! :-) And the 503 HTTP response is specifically defined so user agents (like search engines) can know a site is unavailable and they should check back later. So search engines expect to sometimes see 503's and sometimes see 200's (and different content) from the same website. What I'm recommending to you is indistinguishable from normal web app behavior from the search engine's point of view.
Justin Grant
Persistance is key. I've finally solved this and just posted the solution. Thanks for your suggestions. It's always good to have more than one arrow in the quiver to attack a given problem.
Bryan
+4  A: 

I've found the solution and it's incredibly simple. Turns out the problem was actually in IIS7. While debugging this issue in Visual Studio I saw a property of the HttpResponse object that I hadn't noticed before:

public bool TrySkipIisCustomErrors { get; set; }

This lead me to my nearest search engine which turned up a great blog post by Rick Strahl and another on angrypets.com as well as this question here on SO. These links explain the gory details much better than I can, but this quote from Rick's post captures it pretty well:

The real confusion here occurs because the error is trapped by ASP.NET, but 
then ultimately still handled by IIS which looks at the 500 status code and 
returns the stock IIS error page.

It also seems this behavior is specific to IIS7 in Integrated mode. From msdn:

When running in Classic mode in IIS 7.0 the TrySkipIisCustomErrors 
property default value is true. When running in Integrated mode, the 
TrySkipIisCustomErrors property default value is false.

So essentially all I ended up having to do is add Response.TrySkipIisCustomErrors = true; right after any code that sets the Response.StatusCode to 500 or 503 and everything now functions as designed.

Bryan
+1 Thanks, Bryan. After 2+ hours of searching you gave me the answer I needed. Its so simple. I was trying to set a 404 error code which was giving me a security exception and "Response.TrySkipIisCustomErrors = true;" fixed it.
David Murdoch
This was driving me effing crazy. Thank you so much. Placing "Response.TrySkipIisCustomErrors = true;" immediately after setting response codes in my Error controller is what worked for me. It was misleading because my local IIS 7 instance was rendering my custom error views, but the remote staging server was not. =P
NovaJoe