Using ColdFusion to Handle HTTP 404 Page Not Found Errors

This article will not address the onMissingTemplate method which only runs when a CFML page does not exist (i.e., wwwroot/directory/thisPageDoesNotExist.cfm). In addition, this article will not address using custom headers or a .htaccess file with Apache. This article will address those other "page not found" errors often found in Windows hosted environments.

Let's start by assuming someone tries to visit www.yourdomain.com/thisDirectoryDoesNotExist, or www.yourdomain.com/thisPageDoesNotExist.html and neither the directory, nor the page actually exist on your site. Or, better yet, you've just spent months building a new, dynamic site for a client using ColdFusion and their old site was an old, yucky plain HTML site with pages everywhere. Now, your client, who's a little savvy in the SEO department, (that's search engine optimization for you non-seo-knowing folk), has requested you setup redirects for each of his old pages to the new ones you've created.

If you're anything like me, I used to dread the thought of taking care of this. I used to spend a ton of time on this by creating a separate page for each of the "old" pages and directories, then using either JavaScript to handle the redirects (if the old page was vanilla HTML) or the "old" dynamic language (i.e., ASP, PHP, etc.) to create custom headers, etc. Regardless, it was a major pain to do this, especially because it cluttered my site with a bunch of extra files that were essentially useless.

Then, I had one of those "light bulb moments." I thought, "Hey, I'm already using a custom tag to handle 'page not found' requests, so if I could somehow capture the requested page, match it against a list of 'known-to-be-missing' pages, then I could code a way to forward to the 'new' destination." Sounds simple enough, eh?

For those of us using a shared-host provider such as CrystalTech or HostMySite, they're kind enough to allow us to create custom pages to handle certain HTTP errors, such as 404. It's usually found in the service provider's "control panel" under something like "IIS > Custom Error Pages." Once there, you usually have three options to pick from: 1) Default, 2) File and 3) URL. In most cases, "Default" loads the standard error page provided by our friends at Microsoft. "File" is just that, a flat, static, HTML page. What we want to use is "URL," so that we can use ColdFusion to help us out. In fact, I'll be nice and even provide a link to both CrystalTech's process and HostMySite's process. But before you do this, you'll need to at least set up a .CFM file somewhere on your site so you know what to enter into the URL path, right? So for now, let's assume you've created a file call "404.cfm" and placed in a directory structure like so: "/extensions/customtags/404.cfm"

So, assuming you've got your custom error page setup properly, you will now be able to capture a query string which contains the "requested URL" that really didn't exist. The easiest way to test this would be to enter this code onto your 404.cfm file, upload it to your site, then try a non-existent directory such as www.yourdomain.com/abcd/.


<cfset request.queryString = getPageContext().getRequest().getQueryString() />
<cfoutput>#request.queryString#</cfoutput>
<cfabort />

You should see something like this: "404;http://www.yourdomain.com:80/abcd/"

There's some great information coming through here. First, you'll notice that you're receiving the error number (404). Next, you'll see your domain also includes the port number being used. For example, if using HTTP, then you will most likely see the number 80, or if your using HTTPS, then you should probably see 443.

For this next part, you might have to adjust this if you ever use a colon ( : ) in your directory or page naming conventions (this is not generally used, so I wouldn't expect this to be much of an issue).

Look again at the query string that we've been given, what we really want to grab is everything after the port number. Or, the absolute path to the page or directory being requested. So this is what I came up with:


<!--- perform some manipulations to the 'requested url' to get at the actual request --->
<cfif isDefined("request.queryString")>
    <cfset requestedPage = listlast(request.queryString, ":") />
</cfif>
<cfif len(trim(requestedPage))>
    <!--- after removing everything before the port, we now need to remove the port number --->
    <cfset requestedPage = listdeleteat(requestedPage, "1", "/") />
    <!--- quick fix to create an absolute path from the site root to the requested page --->
    <cfset requestedPage = "/" & requestedPage />
</cfif>
<cfoutput>#requestedPage#</cfoutput>
<cfabort />

So, now if we use the same url we tried earlier, I should see something like: "/abcd/" Now, I've got a variable that I can use to match against to see if this is a "known" directory or page from our old site. Now we just need something to handle the "matching" and then receive a response to make decisions against.

Luckily, I've created a rather simple .CFC which can easily be modified to accommodate any "known" directories and/or pages, including .ASP, .PHP, etc.


<!------------------------------------------------------------------------------------------------------------

    Document:        /extensions/components/redirect.cfc
    Author:            Steve Withington
    Creation Date:    12/30/2008
    Copyright:        (c) 2008 Stephen J. Withington, Jr. | www.stephenwithington.com
    
    Purpose:        Handles redirects of old pages to new ones.

    METHODS/VAR:    1) getLocation() / newLocation
    
    Revision Log:    
    MM/DD/YYYY - sjw - comments.

------------------------------------------------------------------------------------------------------------->

<cfcomponent displayname="Handles redirects" hint="Pass me an old location, I'll give you the new location." output="no">

    <!---    1) getLocation()    --->
    <cffunction name="getLocation"
                    displayname="Get a new location for old links."
                    access="public"
                    returntype="string"
                    output="no">

    
        <cfargument name="oldLocation" required="false" default="" />
    
        <cfset var newLocation = "" />
    
        <cfswitch expression="#arguments.oldLocation#">

            <cfcase value="/index.html">
                <cfset newLocation = "/index.cfm" />
            </cfcase>

            <cfcase value="/about/news/default.php,/about/news/,/about/news">
                <cfset newLocation = "/news/index.cfm" />
            </cfcase>

            <cfcase value="/contact-us.asp">
                <cfset newLocation = "/contact/index.cfm" />
            </cfcase>

            <cfdefaultcase>
                <cfset newLocation = "" />
            </cfdefaultcase>

        </cfswitch>
    
        <cfreturn newLocation />
    
    </cffunction>

</cfcomponent>

Now, we just need to invoke the ColdFusion Component to see if there's a match.


<!--- check to see if the page requested matches any 'known' pages from the old site --->
<cftry>

    <cfinvoke component="extensions.components.redirect" method="getLocation" returnvariable="newLocation">

        <cfif isDefined("requestedPage") and len(trim(requestedPage))>
            <cfinvokeargument name="oldLocation" value="#requestedPage#" />
        </cfif>

    </cfinvoke>

    <cfif isDefined("newLocation") and len(trim(newLocation))>
        <cfset newPage = newLocation />
    </cfif>

    <cfcatch>
        <cfset newPage = "" />
    </cfcatch>

</cftry>

As you can see, if no match has been found, a variable called "newPage" is merely left blank. So, now we can write a little more code to accommodate both "known-to-be-missing" and "truly-unknown-and-really-missing" directories and pages. If a page is "known-to-be-missing", I can pass a search engine friendly header so that everything runs as smoothly as possible.

Here's the final document, aside from the .CFC above which should be kept separate.


<cfsilent>
<!------------------------------------------------------------------------------------------------------------

    Document:        /extensions/customtags/404.cfm
    Author:            Steve Withington
    Creation Date:    12/30/2008
    Copyright:        (c) 2008 Stephen J. Withington, Jr. | www.stephenwithington.com
    
    Purpose:        Handles requests for missing pages (HTTP 404).

    Notes:            Test this set up before using in a live environment. The 404 custom error page needs to be
                    set up in IIS (or via control panel in a third-party hosted environment).
    
    Revision Log:    
    MM/DD/YYYY - sjw - comments.

------------------------------------------------------------------------------------------------------------->

<!--- scope local variables --->
<cfparam name="requestedPage" default="" />
<cfparam name="newPage" default="" />
<!--- use a little of the underlying java to grab the queryString --->
<cfset request.queryString = getPageContext().getRequest().getQueryString() />
<!--- perform some manipulations to the 'requested url' to get at the actual request --->
<cfif isDefined("request.querystring")>
    <cfset requestedPage = listlast(request.queryString, ":") />
</cfif>
<cfif len(trim(requestedPage))>
    <!--- after removing everything before the port, we now need to remove the port number --->
    <cfset requestedPage = listdeleteat(requestedPage, "1", "/") />
    <!--- quick fix to create an absolute path from the site root to the requested page --->
    <cfset requestedPage = "/" & requestedPage />
</cfif>
<!--- check to see if the page requested matches any 'known' pages from the old site --->
<cftry>
    <cfinvoke component="extensions.components.redirect" method="getLocation" returnvariable="newLocation">
    <cfif isDefined("requestedPage") and len(trim(requestedPage))>
        <cfinvokeargument name="oldLocation" value="#requestedPage#" />
    </cfif>
    </cfinvoke>
    <cfif isDefined("newLocation") and len(trim(newLocation))>
        <cfset newPage = newLocation />
    </cfif>
    <cfcatch>
        <cfset newPage = "" />
    </cfcatch>
</cftry>
</cfsilent>
<cfif isDefined("newPage") and len(trim(newPage))>
<!--- requested page is a known 'old page' from prior site --->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<cfheader statuscode="301" statustext="Moved permanently" />
<cfheader name="Location" value="http://www.yourDomainHere.com#newPage#" />
<title>This page has moved to http://www.yourDomainHere.com<cfoutput>#newPage#</cfoutput></title>
</head>
<body>
This page has moved to <cfoutput><a href="http://www.yourDomainHere.com#newPage#">http://www.yourDomainHere.com#newPage#</a></cfoutput>
</body>
</html>
<cfelse>
<!--- requested page is NOT a known 'old page' from prior site and doesn't exist --->
<cf_layout     title="Hmm, the page you're looking for can't be found."
                keywords="404,page,not,found"
                description="Hmm, the page you're looking for can't be found. You may have clicked a bad link or mistyped the web address.">

    <h1>Hmm, the page you're looking for can't be found.</h1>
    <p>You may have clicked a bad link or mistyped the web address.</p>
    <ul>
        <li><a href="/">Return home</a></li>
        <li><a href="javascript:history.back();">Go back to the previous page</a></li>
    </ul>
</cf_layout>
</cfif>

As you can see, I like to use a custom tag to handle the layout of my pages, etc. You can find out more about that at Raymond Camden's site.

I hope this helps a fellow ColdFusion developer. Thanks for reading!

Comments

Great info Steve! This is a wonderful solution for CF SEOs. I'll be sure to send my team here to read.
# Posted By Jack Leblond | 12/30/08 6:57 PM
@Steve

Hey man, that final code example looks a bit fubarred. Nice post though.
# Posted By Steve 'Cutter' Blades | 1/2/09 1:06 PM
@Steve,

I think ColdFish (http://coldfish.riaforge.org/) is having trouble parsing some of my code examples. Sometimes, a refresh fixes it though.

Thanks for the comments!
# Posted By Stephen Withington | 1/2/09 1:25 PM
I found another article which might help people trying to handle 404 errors on Apache: http://blog.sixsigns.com/2008/02/04/custom-404-pag...
# Posted By Stephen Withington | 1/6/09 10:07 AM
While I can seem to make things work for a non-existant htm page, any non existant .cfm page gives me a CF Error page. I cannot seem to figure out how to make it retrieve the page set as the custom error page for 404 errors in IIS if the non existant page is a .cfm page.
# Posted By John K | 1/26/09 2:50 PM
@John K,

You're correct, this method will work for nearly ANY file type _except_ .CFM files.

In order to handle missing ColdFusion templates (.CFM files), I recommend using the onMissingTemplate() function available in your Application.cfc.

For example:

   <!--- onMissingTemplate() --->
   <cffunction name="onMissingTemplate" returnType="boolean" output="false">
   
      <cfargument   name="thePage" type="string" required="true" />

      <cftry>
         <!--- log error here if you wish --->
         <!--- after logging, then redirect the user to a friendly message --->
         <cflocation url="/404.cfm?thepage=#urlEncodedFormat(arguments.thePage)#" addToken="false" />
         <cfreturn true />

         <!--- if an error occurs, return false and the default error handler will run. --->
         <cfcatch>
            <cfreturn false />
         </cfcatch>
      </cftry>

   </cffunction>

At this time, I usually maintain a separate page very similar to the 404 handler I use in my article ... but I place this one at the root of my site. Using the final document I listed above, you could then create a new file called 404.cfm, then save it to your site root. You'll need to alter a few lines of code in the beginning for it to work properly though.

Since you'll be receiving a clean query string via the URL from onMissingTemplate method, (i.e., URL.thePage), you won't need to do any queryString manipulation upfront anymore.

So you could just remove everything down to the <cftry> block that invokes the component and place something like this above it:

   <cfparam name="URL.thepage" default="" />
   <cfparam name="requestedPage" default="" />
   <cfparam name="newPage" default="" />

   <cfif isDefined("URL.thepage") and NOT len(trim(URL.thepage))>
      <cflocation url="/" addtoken="no" />
   </cfif>

   <cfset requestedPage = URL.thepage />

Hope this helps! Let me know if you have any other questions.
# Posted By Stephen Withington | 1/26/09 4:27 PM
Thanks Stephen,

I was playing with placing some code in Application.cfc, but as I only had an 'application.cfm' in my root, I created an Application.cfc from the application.cfm. Didn;t seem to work. As I am running CF7.02 Server I assume is should recognize a '.cfc' file, but after I removed the '.cfm' version of it, I got an error so I had to restore it.

I'm going to try and place your code in the 'application.cfm' file and see if I get any joy, at the moment I have this which of course doesn;t do anything:

<cferror type = "exception"
exception = "Any"
template = "404.cfm"
mailto = "sales@mparam.com">

<cffunction name="onMissingTemplate" returnType="boolean">
<cfargument type="string" name="targetPage" required=true/>
...
<cfreturn BooleanValue />
</cffunction>
# Posted By John K | 1/26/09 4:59 PM
Follow Up:

I seem to be thick on etnering the correct parameters, sorry about my ignorance. It does appear that CF is finding the '.cfc' file. Here is the error I now see:

Invalid name for argument.
The name 404.cfm used for an argument has illegal characters in it.

The error occurred in C:\Sites\Q10.ca\Application.cfc: line 48

46 : <cffunction name="onMissingTemplate" returnType="boolean" output="false">
47 :
48 : <cfargument name="404.cfm" type="string" required="true" />
49 :
50 : <cftry>

Is it caused by this code I place in '404.cfm'?
<cfparam name="URL.404.cfm" default="" />
<cfparam name="404.cfm" default="" />
<cfparam name="404.cfm" default="" />

<cfif isDefined("URL.thepage") and NOT len(trim(URL.thepage))>
<cflocation url="/" addtoken="no" />
</cfif>

<cfset requestedPage = URL.404.cfm/>

Thanks for the assitance!

# Posted By John K | 1/26/09 5:18 PM
@John,
I think you might be a little confused. Use the code as I've provided it and don't change the names or values of anything. For example, you have <cfargument name="404.cfm" type="string" required="true" />

Instead, use the <cfargument name="thePage" type="string" required="true" /> that I provided and so forth. Also don't change the names or values of the cfparam's or cfset's that I've provided either. There's no need to make them any other values. Also, instead of using <cfreturn BooleanValue />, use the code I've provided. The onMissingTemplate method's returnType is set to "boolean" which means the application is expecting a boolean value (i.e., true or false, yes or no, 1 or 0) but you're attempting to return an undefined variable called "BooleanValue"

In the end, you should have an Application.cfc file and a 404.cfm file at your site root.

If you still can't get it to work, let me know.
# Posted By Stephen Withington | 1/26/09 6:44 PM
@John,
I just realized you mentioned you're _not_ running on CF8 ... unfortunately, onMissingTemplate is a CF8 method.
# Posted By Stephen Withington | 1/26/09 6:50 PM
Oh no! OK thanks. I guess I'll hunt for something else that will work then. THanks!
# Posted By John K | 1/26/09 7:16 PM
Really helpful - I've been looking for something like this for a while.
Thinking I might link it up with a database to allow for many records.
# Posted By Gary | 1/30/09 4:37 PM
Someone in the Adobe CF Forum helped me setup IIS so at least my '404.cfm' page would also show up as the redirect for CF Pages which is the best workaround while I wait to see if I can get CF8. In the meantime if anyone needs that IIS workaround, you should be able to see it here:

http://www.adobe.com/cfusion/webforums/forum/messa...
# Posted By John K | 1/30/09 4:53 PM
Okay, I know the post is old but I've been searching for a way to create a custom error system for 404/301 errors. After reading multiple articles on how to accomplish this I ended up combining several snippets of code from multiple web sites to achieve something somewhat unique. I placed my code in the Application.cfc as a onMissingTemplate function. Once the page the was trying to load from Google was called, it then processed this function because the page had been moved or no longer existed.

Next, when the function called the error template with a 404 status I placed a 301 status in the error template which in turn forced the page link that was missing to go from a 404 to a 301 status. I then used the header location to redirect to my home page.

Now I know that it started out as a 404, but this seemed to be the only way to get bad links on Google to be redirected as a 301 status and load a good page on my web site. I tried using the normal header tag method described by most of the web sites I found when doing a search for a 301 redirect, but non of the seemed to work or it kept me in a continuous loop.

The only way I will truly know if this works or not is when my site gets re-indexed. I'm thinking that CF should allow for 301 errors and other errors and not just the capability of the onError and onMissingTemplate functions.
# Posted By Neo Symmetry | 9/13/10 12:47 AM
Old thread, but still subscribed to, so your efforts are not in vain. Interesting to see how it works out. Thanks!
# Posted By JohnK | 9/13/10 1:56 AM
I have used a (very) similar system for a few years.

However, I recently checked on it with Firebug, and discovered that it isn't quite working as I'd intended. I wonder if your system described above works any differently.

Here's the problem: The initial redirect is to the script set up in IIS as the 404 error handling URL. This redirect is shown in Firebug as a 302 (Moved Temporarily).

Then, after that initial redirect, the redirector script performs the 301 (Moved Permanently) redirect.

So, it appears that the obsolete file or script is noted as moved "Temporarily" to the redirector.

Does that mean that this scheme actually doesn't work as intended?

- Don C.
# Posted By Don C | 10/24/11 3:58 PM

© 2024, Stephen J. Withington, Jr.  |  Hosted by Hostek.com

Creative Commons License   |   This work is licensed under a Creative Commons Attribution 3.0 Unported License.