Using ColdFusion to Handle 404 Errors (Page Not Found) On Development Server
Handling 404 errors can be tricky no matter what. On your live, production server, you can set up custom 404 handling on a site-by-site basis via IIS custom error templates. However, for many of us, this is trickier on the development server where there are no site. Its just one web server with many sub directories, none of which are usually virtual sites.
To remedy this situation, I have developed a 404 error handler that is defined at the root of the developmental web server. This 404 error handler catches 404 errors and attempts to forward the errors to individual "site" error handlers in an attempt to mimic 404 error handling on a live site. This setup makes a few assumptions:
- Your 404 errors are handled by a ColdFusion template named "cferror.cfm"
- Your cferror.cfm template is located either in the root of the site (a sub directory on the development server) or at least that it is in the same directory or a parent directory of the requested non-existent page.
The system works by catching the 404 error at the root of the ColdFusion web server. It then takes the requested page and starts crawling up the directories url. For each directory, it check for the existence of the cferror.cfm template. If it is found, the root 404 error handler performs a CFLocation to that cferror.cfm template, sending, as the query string, the same "404;" query string that IIS has defined.
As the system crawls up the directory path, after checking for cferror.cfm, it checks for either an Application.cfm or Application.cfc file. If it hits one of these files, it stops crawling and displays its own Page Not Found message. The idea here is that we don't want to leave the context of the application when search for a 404 error handler.
I have prevented infinite loops (the root 404 error handler constantly CFLocating to itself) by stopping the search once there is no more directory to crawl (signaling that we are in the root directory). I have not tested this, but I assume this means that if the development server's cferror.cfm template is NOT IN THE ROOT of the web server, the infinite looping IS possible.
A word of caution: This template is done to help develop 404 error handling by simulating 404 error handling on the live, production server. It DOES NOT MIMIC live 404 error handling. On the live server, IIS will pass control directly to your site's 404 error handler without doing any URL location changes. Therefore, from your BROWSER's PERSPECTIVE, on the live server, your missing page URL may be in a different directory as your 404 error handler. On the development server, however, the missing page URL is ALWAYS in the same directory as the 404 error handler.
For example, let's say you request a non-existent page "ben-nadel/is/cool.htm" on the live server.
Browser sees:
ben-nadel/is/cool.htm
ColdFusion sees:
cferror.cfm
The web browser is TWO directories down in the site structure. The relative paths would start with "../../".
If you do the same thing on the development server:
Browser sees:
cferror.cfm?404;http://..../ben-nadel/is/cool.htm
ColdFusion sees:
cferror.cfm
Both the web browser and the 404 error handler are in the SAME directory (due to the root 404 error handler's CFLocation). The relative paths would be empty since there is no sub-folder URL.
The cferror.cfm template as it exists in the root of my development server:
<!---
Set a short timeout since this page should not
hog any resources.
--->
<cfsetting requesttimeout="5" />
<!--- Get the query string. --->
<cfset strQueryString = CGI.query_string />
<!--- Check to make sure that we have a 404 error. --->
<cfif Find( "404;", strQueryString )>
<!--- Strip out server name. --->
<cfset strDirectoryPath = ReplaceNoCase( strQueryString, CGI.server_name, "", "ONE" ) />
<!--- Strip out 404 and protocols. --->
<cfset strDirectoryPath = REReplaceNoCase( strDirectoryPath, "(404;)|[a-z]{2,5}://", "", "ALL" ) />
<!--- Remove any query strings. --->
<cfset strDirectoryPath = REReplace( strDirectoryPath, "(\?.*)$", "", "ONE" ) />
<!---
If there is a file being used, get the directory
from teh path.
--->
<cfif REFind( "\.[\w]*$", strDirectoryPath )>
<!--- Remove the file name. --->
<cfset strDirectoryPath = GetDirectoryFromPath( strDirectoryPath ) />
</cfif>
<!---
Set the initial current directory value that will
be check during the loop.
--->
<cfset strCurrentDirectory = strDirectoryPath />
<!---
Loop over the directory path while we still have
directories and look for the cferror.cfm.
--->
<cfloop condition="true">
<!--- Get the directory from the path. --->
<cfset strCurrentDirectory = GetDirectoryFromPath( strCurrentDirectory ) />
<!---
Check to see if we need to break. We don't
want to find THIS cferror.cfm page as that
will just cause a crazy infinite loop. At this
point, just let the page finish to show the
page not found error.
--->
<cfif (Len( strCurrentDirectory ) LTE 1)>
<cfbreak />
</cfif>
<!--- Check to see if the cferror.cfm file exists. --->
<cfif FileExists( ExpandPath( strCurrentDirectory & "cferror.cfm" ) )>
<!---
We found an error template, so relocate to
that template. To make sure our paths are not
crazy, lets cflocation to it and use the same
query string that we got.
CAUTION: This is not how the LIVE server will
handle this action. This is designed for local
error handling on the developmental serer.
--->
<cflocation url="#strCurrentDirectory#cferror.cfm?#CGI.query_string#" addtoken="no" />
<cfbreak />
<!--- Check to see if we can find an application file. --->
<cfelseif (
FileExists( ExpandPath( strCurrentDirectory & "Application.cfm" ) ) OR
FileExists( ExpandPath( strCurrentDirectory & "Application.cfc" ) )
)>
<!---
We didn't find a cferror.cfm page, but we did
hit the root of an application, so break out
of this loop. We don't want to crawl up the
directory any more than we have too. If this
application doesn't catch errors then just stop.
--->
<cfbreak />
<cfelse>
<!---
We didn't find the file or hit any application
roots, so remove the right most slash for next
loop. We do this otherwise the
GetDirectoryFromPath() will not be able to keep
moving up path.
--->
<cfset strCurrentDirectory = REReplace( strCurrentDirectory, "[\\/]{1}$", "", "ALL" ) />
</cfif>
</cfloop>
</cfif>
<!---
If we are still here, then we didn't find a directory
containing a cferror.cfm. Dump out some info to help
the user debug.
--->
<h2>
404 Page Not Found
</h2>
<p>
<cfset WriteOutput( CGI.query_string ) />
</p>
<cfdump var="#CGI#" />
Want to use code from this post? Check out the license.
Reader Comments
know this post is old but this was exactly what I was looking for. Awesome
@Joshua,
Awesome. Never too late :)
Ben, is this solution appropriate for my production server running about 80 sites? I provide a ColdFusion CMS and each site is an instance (still in their own directory on the prod server) and I am looking for global error handling. Basically want to show the friendly message to the user and email or stare the actual error to me.
Seems something would have to be done in both CF admin and IIS to cover all the bases? (that is, provide a friendly message for any error?)
Providing I wanted to govern all these sites with one solution, can/shold the cferror.cfm file(s) be placed in a common place outside each site root? Thanks for any info.
Jason
@Jason,
I wouldn't use a global error handler to handle ColdFusion errors - I'd set up something like a CFError tag, or the OnError() event handler in each of the given Application.cfc files.
Now, if you're talking about a global missing template handler, then I believe that can be set up in the ColdFusion administrator, but I have not done that before (due to laziness most likely). You have to be careful in that template, however, as it does not support the full array of ColdFusion functionality.
Ben,
Would an onMissingTemplate function in the application.cfc be different from your method?
Thanks!
~kate
@Kate,
The onMissingTemplate() is good, but it only works with CFM/CFC pages; as such, it won't work with anything like .htm pages or even partial URLs that get pasted improperly.
Fantastic!
I'm going to use this all over the place. Well, technically, I'm going to use it in only ONE place, but it's going to be a huge improvement for handling missing pages.
@Leonard,
Recently, I have started using IIS Mod-Rewrite and have found it really nice. But, before that, I was using this 404-handling method with great success for like 4 years.
Hi Ben,
I have a project where we're receiving a list of URL's from auto dealerships to display their car inventory, the problem I'm having is that some of the images don't display because of 404 errors. I'm trying to find a way to display a default placeholder if the URL is bad, but I cannot figure out how to detect the 404 error in the photo URL. Do you have any advice?
Thanks,
Justin
Ben, where we gonna put this template , i mean how this file gets called automatically every time a non existent file is requested.
Ben, Suppose I run a url http://localhost:8500/mytest/blaah.......
it returns me java.io.FileNotFoundException.
But I want to handle this in my customised showing custom error page.
For Apache/Linux:
I use HostMySite, and I set up the 404 handler page. Apply something like my code below on the handler page:
<cfset the404 = cgi.REDIRECT_URL>
<cfif the404 CONTAINS ".php">
<cfset theFileName = listLen(the404,'/')>
<cfset theFileName = listGetAt(the404,theFileName,'/')>
<cflocation addtoken="no" url="/some_directory/#replaceNoCase(theFileName,'.php','.cfm')#">
</cfif>
Very easy the configuration.
I read a lot pages and I can't find the solution.
I open the administrator and change this
Administrator/server settings/Error Handlers/Missing Template Handler
and put the page that you want load and it is missing.
I create a cferror.cfm but it could be a index.cfm
/cferror.cfm
Hey Ben - I know this post is fairly old - but it has helped a great deal in implementing something I need for one of my apps.
I have a scenario I'd like to ask you help with:
Example -
index.cfm has a bad img link. My custom 404 shows the QUERY_STRING value - but the "script_name" is shows the custom 404 page - rather than the index.cfm that contains the bad link. Any clue on how/where I can get that info from my page?
And RIGHT after I ask my question - I come up with me answer. Capturing the CGI.HTTP_REFERER gave me the page (and the URL string) that contains the error...
*come up with MY answer* (Yeesh - I need more coffee today!)
Hey, Ben. I recently had some code moved from a server running IIS in front of ColdFusion to a server running Apache in front of ColdFusion and the move broke my 404 handling because Apache doesn't populate the CGI.QUERY_STRING value the same way IIS does. I know your post here is about IIS but I thought I would post what I did on Apache in case it helps another reader (and then I'll plan to write a blog post of my own about it too).
Once I found out about the Apache .htaccess file and how it works, I decided that rather than rewrite my ColdFusion code, I would use the .htaccess file to populate the same CGI.QUERY_STRING value as would be automatically populated by IIS; this way the code is portable and can be used with ColdFusion running behind either IIS or Apache (because, as well as I'm aware, if you put a .htaccess file onto IIS, it just gets ignored). Here is what I have created to do that:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !\.cfm$
RewriteCond %{QUERY_STRING} !=""
RewriteRule ^(.*)$ redirect.cfm?404;http://%{HTTP_HOST}%{REQUEST_URI}?%{QUERY_STRING}
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !\.cfm$
RewriteRule ^(.*)$ redirect.cfm?404;http://%{HTTP_HOST}%{REQUEST_URI}
I believe the only change anyone should need to make to this code would be to the name of the ColdFusion file being used for handling the redirect: in my code above, it's redirect.cfm; you would need to change it to whatever is the name of the ColdFusion file you're using for handling your redirects.
When searching for info on how to handle this, I found indications that people are struggling with how the CGI.QUERY_STRING value is populated with IIS + ColdFusion 10; that's apparently a bug and this info I have provided here does not of course have any bearing on that (sorry: I wish it did!).