Andornot Consulting
Friday, October 02, 2009 3:21 PM

Handling 404 errors with ASP.NET

by Ted Jardine

As mentioned at the end of my previous post on handling errors with ASP.NET, handling "404 Not Found" errors are particularly problematic (if you haven't read it yet, please do so). And looking around, the vast majority of information out there on it is not complete, misinformed, or flat-out wrong (but I greatly appreciate all efforts!). And I would argue that this is because ASP.NET implementation of 404 error handling is flat-out-wrong. So with my super hero cape on, here I come to wobbly save the day!

The typical ASP.NET way to handle 404 errors is to put something like the following in your Web.config:

<customErrors mode="On" defaultRedirect="~/error.aspx">
    <error statusCode="404" redirect="~/page-not-found.aspx" />
</customErrors>

Make a page-not-found.aspx page and voila! Ya got 'er dun! If you're a little more on the ball, you'll realise that while this configuration works for end users (gives them a pretty page to look at hopefully clearly explaining that you can't find what they're looking for), it's bad for SEO (search engine optimization) because it sends back a 302 temporary redirect to your 404 page which in turn sends back a "200 OK" message. In other words, "Yeehah! No problems here as you've found what you're looking for! Index away!"

So, bright developer that you are, you add in some applicable status code into your 404 page thinking that should take care of it:

protected override void OnLoad(System.EventArgs e)
{
    Response.TrySkipIisCustomErrors = true; // For IIS 7 Integrated Pipeline - see previous post
    Response.Status = "404 Not Found";
    Response.StatusCode = 404;
    base.OnLoad(e);
}

Well, fire up Fiddler and you'll discover that you're still getting a 302 temporary redirect to your 404 page. So you fire up your error handling code and for 404s, you Server.Transfer to your 404 page just like all your other error transfers take place! But no, bafflingly enough, even running through a debug session to ensure you're properly catching your 404, ASP.NET still insists on 302'ing your precious response (although at least now your 404 page is sending back the proper "404 Not Found" error status). So go out there and google everywhere and try every suggestion (just a sampling) and then breathe a prayer of thanksgiving for me and my super-duper super hero cape, because this is how you make your way to the 404 handling bliss (caveat: thoroughly tested only on IIS 7):

First, make something like the following in Application_Error in your global.asax (or better yet, a custom error module):

protected void Application_Error(Object sender, EventArgs e)
{
    Exception exception = Server.GetLastError();
    if (exception is HttpUnhandledException)
    {
        if (exception.InnerException == null)
        {
            Server.Transfer(ERROR_PAGE_LOCATION, false);
            return;
        }
        exception = exception.InnerException;
    }

    if (exception is HttpException)
    {
        if (((HttpException)exception).GetHttpCode() == 404)
        {

            // Log if wished.
            Server.ClearError();
            Server.Transfer(NOT_FOUND_PAGE_LOCATION, false);
            return;
        }
    }

    if (Context != null && Context.IsCustomErrorEnabled)
        Server.Transfer(ERROR_PAGE_LOCATION, false);
    else
        Log.Error("Unhandled Exception trapped in Global.asax", exception);
}

Second, put something like the following in your 404 page (or even better, add it to your above custom error module):

protected override void OnLoad(System.EventArgs e)
{
    Response.TrySkipIisCustomErrors = true; // For IIS 7 Integrated Pipeline - see previous post
    Response.Status = "404 Not Found";
    Response.StatusCode = 404;
    base.OnLoad(e);
}

And lastly, put whatever you want in your customErrors section in your Web.config for 404 - remove the 404 references if you want. The only time it would get used is if you keep the confusion level down by reading that section in your error module to find out what your 404 page is.

And now, you don't get any nasty 302 redirects. You get a blissfully pure and pretty 404.

Note: you can have everything in place as above, but if you forget to Server.ClearErrror(), it'll all be for naught as you'll still get 302 redirected (thanks to http://stackoverflow.com/questions/667053/best-way-to-implement-a-404-in-asp-net for finally helping me get this one nailed).

UPDATE: If for some reason you cannot change your code, a good option to pursue is flipping a switch in your applicationHost.config file that passes your response through without IIS hijacking it. This is the sledgehammer approach, but could be applicable for your situation. For example, in order to allow Umbraco's alternative status code responses to work, this is the only way to get them to avoid being hijacked by IIS (i.e. NotFoundHandlers, booting screens, etc.).

<location path="Site Description">
    <system.webServer>
        <httpErrors existingResponse="PassThrough" />
    </system.webServer>
</location>

Thanks to Fabian Heussser's comment on Rick Strahl's post that helped with this.

Comments

10/16/2009 2:32:13 AM #

Dan Agle

Interesting.  I have a asp.net mvc 1.0 site and tried following this advice. In the Application_Error I trap for a 404 and, rather than reroute to an error controller (which worked fine but has the SOE issue you discuss) I changed it to Server.Transfer to an "Error404.aspx" page.  I included the on page load code as you have it above.

The line Response.Status="404 not Found"; causes a StackOverflowException.

Dan Agle | Reply

10/16/2009 11:08:03 AM #

Ted Jardine

Dan,

I debated caveating that this solution was a WebForms solution only, but I didn't. You might find stackoverflow.com/.../620559#620559 or stackoverflow.com/.../404-http-error-handler-in-asp-net-mvc-rc-5 to be helpful in pointing to a better 404 experience with ASP.NET MVC.

Either way, I'll be testing it out with MVC soon and will post my findings then.

Ted Jardine | Reply

10/16/2009 11:15:25 AM #

Ted Jardine

Dan - after re-reading, I'm trying to determine whether you were able to get it working (as all the ASP.NET MVC samples I've seen involve re-routing to a an error controller) and only blew up when specifying Response.Status = "404 Not Found" in your 404 page (but fine with just Response.StatusCode = 404).

Now I want to drop everything and spike this one! But I'll resist for now. I will say however, that at least in WebForms, you can specify these in your Error Module/global.asax as opposed to your 404 page which is nice because the logic is centralized then.

I am surprised that the re-routing in MVC has the same SEO issues...

Ted Jardine | Reply

Add comment


(Will not be posted. Rather, will be used to show your Gravatar icon.)

  Country flag

biuquote
  • Comment
  • Preview
Loading