Skip to main content
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with: Hemant Khandelwal
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with: Hemant Khandelwal ( @khandelwalh )

Handling 404 Errors in ColdFusion (via IIS Throwing 404)

By on
Tags:

Some one over on the CF-Talk list posted a question about handling 404 errors in ColdFusion without using any sort of ISAPI_REWRITE or MOD_REWRITE. This is the way I do it. It is not perfect, but it works pretty darn well for my small, personal site.

The code here is rather long, so I have posted it to snippets as well.

<!---

	For this example, Let's assume that the URL was:

	http://www.bennadel.com/go/prettyurl/

	This will be used in my comments below.

--->



<!---
	Check to see if this was due to a 404 error. We might be accessing
	this page via the Application.cfc onError event.
--->
<cfif Find( "404;", CGI.query_string )>

	<!---
		This is a 404 error. Now we have to go about figuring out just
		what the error was intending. Right now, the error string is in the
		form of:

		404;http://www.bennadel.com:80/go/prettyurl/
	--->

	<!---
		Get the incorrect URL from the query string (which IIS has
		thrown). This should start with "404;". It might also contain
		the port number (ex. :80) after the domain extension. We want
		to strip those out. We also want to strip out the "www."
		since it might not be there.
	--->
	<cfset strTargetUrl = LCase(
		REReplace(
			CGI.query_string,
			"404;|:80|www\.",
			"",
			"ALL"
			)
		) />

	<!---
		ASSERT: strTargetUrl should now be in the form of:
		http://bennadel.com/go/prettyurl/
	--->


	<!---
		Get the site url. We want to strip out any www from it. This
		way the site url *should* be part of the string we found above
		(where we also stripped out "www").

		NOTE: My url is stored in a config object... but you can get that
		value from anywhere (or even hard code it right here). It is
		http://www.bennadel.com/
	--->
	<cfset strSiteUrl = LCase(
		Replace(
			APPLICATION.ServiceFactory.GetConfig().GetUrl(),
			"www.",
			"",
			"ALL"
			)
		) />

	<!---
		ASSERT: strSiteUrl should now be in the form of:
		http://bennadel.com/
	--->


	<!---
		Now that we have the target url and the site url, we want to
		remove the site url from the target url so that we can isolate
		the script name that was being accessed.
	--->
	<cfset strTargetUrl = Replace(
		strTargetUrl,
		strSiteUrl,
		"",
		"ONE"
		) />


	<!---
		ASSERT: At this point, the strTargetUrl should hold the suffix url that
		was trying to be called. That is, the url of the page minus the site domain:

		go/prettyurl/

		CAUTION: At this point, the page may contain query params (..?foo=bar).
	--->


	<!---
		Check to see if we have any query params. Since the 404 error
		passes the entire script name AND query string into the CGI
		query_string, we have to manually pull out the query string
		values ourself.
	--->
	<cfif Find( "?", strTargetUrl )>

		<!--- We have query string values. Get the query params. --->
		<cfset strTargetQueryParams = ListRest(
			strTargetUrl,
			"?"
			) />

		<!---
			Now that we have the target query params, we can remove
			them from the target page.
		--->
		<cfset strTargetUrl = ListGetAt( strTargetUrl, 1, "?" ) />

	<cfelse>

		<!--- There are no query params. Set a blank value. --->
		<cfset strTargetQueryParams = "" />

	</cfif>


	<!---
		Make sure all the slashes are web slashes. This should already
		be the case, but this is a safe-guard.
	--->
	<cfset strTargetUrl = REReplace(
		strTargetUrl,
		"[\\/]+",
		"/",
		"ALL"
		) />

	<!--- Strip out trailing or leading slashed. --->
	<cfset strTargetUrl = REReplace(
		strTargetUrl,
		"^[\\/]+|[\\/]+$",
		"",
		"ALL"
		) />


	<!---
		We need to ge the target directory. Check to see if we are
		attempting to hit a file or a directory in the target url.
	--->
	<cfif REFind( "\.[\w]+$", strTargetUrl )>

		<!---
			The target item ends in a file ext. This must be a file.
			Get the base directory from the file name and remove the
			ending slash.
		--->
		<cfset strTargetDirectory = REReplace(
			GetDirectoryFromPath( strTargetUrl ),
			"[\\/]+$",
			"",
			"ONE"
			) />

		<!---
			Get the target script name to be the target url. This
			will have the directory AND file.
		--->
		<cfset strTargetScriptName = strTargetUrl />

	<cfelse>

		<!---
			We are not attempting to access any file, just a directory.
			Grab that directory as the target directory.
		--->
		<cfset strTargetDirectory = strTargetUrl />

		<!---
			Since we are pointing to a directory, just grab that as
			the script name as well.
		--->
		<cfset strTargetScriptName = strTargetUrl />

	</cfif>


	<!---
		ASSERT: At this point, we have both :
		- target url
		- target directory
		- target query params

		that were attempted to get called. The target url does
		NOT have any leading or trailing slashes, but it might
		have a file name.
	--->


	<!---
		Not that we have all that stuff, we have to figure out
		what all that means to us on the LOCAL setup. IE, what
		the fake url map to in our framework. Let's test the
		tartet url against some regular expressios.
	--->
	<cfsavecontent variable="strXmlRedirectExpressions">

		<!---
			In order to narrow down the regular expression that
			we have to run, I am checking the first item in the
			target url.
		--->
		<cfswitch expression="#LCase( ListFirst( strTargetUrl, '/' ) )#">

			<!---
				FOR THIS DEMO i am putting the XML here. In
				reality, I am pulling in an xml file form
				each section so that each section can fine
				tune it's own redirection.

				ex:
				<cfinclude
					template="content/go/_url_redirect.xml.cfm"
					/>

				For the demo, I have included it in the proper case.
			--->

			<cfcase value="go">

				<redirect
					in="^go/ben-?nadel\b.*$"
					out="go.bennadel"
					/>

				<redirect
					in="^go/pretty-?url\b.*$"
					out="go.demo404"
					/>

				<!---
					Notice in this one how I am using a reg-exp
					group reference.
				--->
				<redirect
					in="^go/pretty-?url/([0-9]{4})/\b.*$"
					out="go.demo404&search_year=\1"
					/>

			</cfcase>

		</cfswitch>


		<!---
			After the individual cases, I include a global 404
			handler in case none of the others make it.
		--->
		<redirect
			in=".+"
			out="home.display"
			/>

	</cfsavecontent>


	<!--- Trim the value on the XML. --->
	<cfset strXmlRedirectExpressions = Trim(
		strXmlRedirectExpressions
		) />


	<!---
		Check to see if there is a pretty url redirect expression
		list that we can use to test the target url.
	--->
	<cfif Len( strXmlRedirectExpressions )>

		<!--- Parse the expressions into an xml document. --->
		<cfset xmlRedirectExpressions = XmlParse(
			"<redirects>" &
			strXmlRedirectExpressions &
			"</redirects>"
			) />

		<!--- Get query string children. --->
		<cfset xmlChildren = xmlRedirectExpressions.XmlRoot.XmlChildren />

		<!--- Loop through expressions to see if any match. --->
		<cfloop index="intChild" from="1" to="#ArrayLen( xmlChildren )#" step="1">

			<!--- Get reference to this child's attributes. --->
			<cfset objXmlAttributes = xmlChildren[ intChild ].XmlAttributes />

			<!---
				Check to see if we found a match. Use the regular
				expression in our redirects XML and test it against
				the target URL.
			--->
			<cfif REFind( objXmlAttributes.In, strTargetUrl )>

				<!--- Get the mapped action (the OUT xml attribute). --->
				<cfset strTargetAction = REReplace(
					strTargetUrl,
					objXmlAttributes.In,
					objXmlAttributes.Out,
					"ONE"
					) />

				<!---
					Check to see if we have any query params as part of
					the target action string.
				--->
				<cfif Find( "&", strTargetAction )>

					<!---
						Add the query params to the target query params that
						we got from the original 404 error url.
					--->
					<cfset strTargetQueryParams = ListAppend(
						strTargetQueryParams,
						ListRest( strTargetAction, "&" ),
						"&"
						) />

					<!---
						Get rid of the query string part of the
						target action since we just copied it
						over to the target query params.
					--->
					<cfset strTargetAction = ListFirst(
						strTargetAction,
						"&"
						) />

				</cfif>

				<!---
					We found a regular expression match to the
					target URL. We don't need to keep searching
					so break out of the loop.
				--->
				<cfbreak />

			</cfif>

		</cfloop>


		<!---
			ASSERT: At this point, we have the:

			- target url
			- the target query params
			- mapped action (based on the reg-exp)
		--->


		<!---
			Update script name based on the error. Since we cannot
			update the CGI.script_name value directly, I am storing
			the target "script name" in a custom variable.

			I keep a struct called Environment (CFC), but this could
			be any variable that you reference in the page processing.
		--->
		<cfset REQUEST.Environment.OverrideScriptName(
			GetDirectoryFromPath( CGI.script_name ) &
			strTargetScriptName
			) />


		<!---
			Remove the 404 error from the attributes. This is
			custom struct in my framework that combines the
			URL and FORM variables.
		--->
		<cfloop item="strKey" collection="#REQUEST.Attributes#">

			<cfif NOT Compare( "404;", Left( strKey, 4 ) )>
				<cfset StructDelete( REQUEST.Attributes, strKey ) />
			</cfif>

		</cfloop>


		<!---
			Now, we need to move any target query params into my
			framework's attributes scope. Since I never reference
			URL for FORM directly, I do NOT bother updating them
			at this point, but you could certainly set URL values
			here.
		--->

		<!--- Update the attribute values. Get the array of params. --->
		<cfset arrQueryParams = ListToArray(
			strTargetQueryParams,
			"&"
			) />

		<!---
			Loop over the query param pairs and add them to the
			request attributes scope.
		--->
		<cfloop index="intPair" from="1" to="#ArrayLen( arrQueryParams )#" step="1">

			<!--- Get the pair. --->
			<cfset arrPair = ListToArray( arrQueryParams[ intPair ], "=" ) />

			<!--- Make sure we have two items. --->
			<cfif (ArrayLen( arrPair ) NEQ 2)>
				<cfset arrPair[2] = "" />
			</cfif>

			<!--- Set the attributes value. --->
			<cfset REQUEST.Attributes[ arrPair[1] ] = arrPair[2] />

		</cfloop>



		<!---
			THIS NEXT IF STATEMENT IS PART OF MY FRAMEWORK. I DO NOT USE
			ABSOLUTE URLS IN MY APP. ALL MY URLS ARE RELATVE (IE. ../../../).
			BECAUSE OF THIS, I NEED TO UPDATE WHAT THE SERVER THINGS THE
			WEB BROWSER IS SEEING. SINCE THE SERVER IS IN THE ROOT AT
			THIS PAGE (site_error.cfm) AND THE WEB BROWSER IS IN A SUB
			DIRECTORY, THE TWO PATHS DO NOT LINE UP.

			HOWEVER, DUE TO THE WAY MY 404 HANDLER WORKS ON DEV, I HAVE TO
			DO THIS DIFFERENT ON THE DEV AND LIVE SERVERS.
		--->

		<!---
			Check to see why we are on the site_error.cfm page. If we are,
			then we were thrown directly to it (probably on the
			developmental server). In this case, use the appropriate web
			root (which would be ""). However, if we are not on that page,
			then we probably go sent here from another page (probably on
			the live server).
		--->
		<cfif APPLICATION.ServiceFactory.GetConfig().GetIsLive()>

			<!--- We are live, get the webroot based on the query string. --->
			<cfset REQUEST.Environment.Web.Root = RepeatString(
				"../",
				ListLen( strTargetDirectory, "/" )
				) />

		</cfif>


		<!--- We do, so set the header to be proper code. --->
		<cfheader
			statuscode="200"
			statustext="OK"
			/>

		<!--- Store the target action. --->
		<cfset REQUEST.TargetAction = strTargetAction />

		<!--- Include the index file. --->
		<cfinclude template="index.cfm" />


		<!---
			We have just include the main site controller (index.cfm)
			We DO NOT WANT the rest of this template execute.
		--->
		<cfexit />


	<!---
		There was no matching Regular Expression file for this
		html. Therefore, we are going to state that this page
		was reached in error.
	--->
	<cfelse>


		<!--- If we are live. Send an email to alert error. --->
		<cfif APPLICATION.ServiceFactory.GetConfig().GetIsLive()>

			<cfmail
				to=""
				from=""
				subject="Error Page Reached"
				type="HTML">

				#CGI.script_name#<br />
				#CGI.query_string#<br />
				<br />

				<cfdump var="#CGI#" />
				<cfdump var="#REQUEST#" />
			</cfmail>

		</cfif>


	</cfif>


</cfif>


<!---
	ASSERT: This page was reached in error. No 404 error was
	mapped. Either someone has a bad link or they are
	trying to hack my site!
--->


<!--- DISPLAY STANDARD HTML PAGE HERE. --->

<cfabort />

Want to use code from this post? Check out the license.

Reader Comments

3 Comments

Ben,
I have downloaded your example and it looks very straight forward. Thanks for the help on handling 404 errors. I am trying to implement this method now on a IIS5 / W2K box. I am having trouble getting IIS to call my error handling page.

Here is what I did. I took your snippet, created a new file, 404handler.cfm. I then went into IIS, went to the web site I am testing and changed the Customer Error 404 to my new 404handler.cfm.

Problem is, is that IIS is not sending my 404_handler.cfm to CF for processing, all I get on my page is the output I requested (I wanted to see the cgi variable you are parsing). So, question is, what did I do wrong in setting up my error 404 handler in IIS?

Thanks,
Dave Hatz

15,640 Comments

Dave,

Bear with me and debugging this as IIS is not my speciality. I know that when you set up IIS to handle 404 errors, you have several options to choose from (ie. File, URL, and I think one other)... what did you choose?

15,640 Comments

Dave,

This might sound sad, but right now, there is no one here who as access to IIS, so I can't check how we do it. I am pretty sure that we use URL:

/cferror.cfm

On the live server, this usually isn't a problem. On the local server (where we don't have individual sites set up in IIS), we have to modify it a bit:

www.bennadel.com/index.cfm?dax=blog:108.view

This puts the error handler in the root of the sever, which in turn catches the error and crawls up the directory looking for a local cferror.cfm.

3 Comments

Ben,
You da man, that was it. Set the Error Template to type of URL and that did the trick.

Thank you very much for your help.

Dave Hatz

4 Comments

This works on Windows 2000, but not on 2003. The posted values (form) are not passed to the 404 handler it seems..

Or is it just me?

4 Comments

Hi Ben,

Thanks for your reply.

There's one notable difference. On 2000, the error number when there is a form posted to a non exist file in 403. (Method not allowed). On 2003, it is the 404.

I ran the HTTP analyser to try to find a difference between both server but there isn't.

I made a test also just trying to get the form value, but they were not defined.

So it seem that the value are simply not passed.

But there must be a way ...

15,640 Comments

@Etienne,

That is too weird. I find it hard to believe that the page-not-found error would have different numbers on different versions of a server. Those numbers are a global standard (I think) and their meaning is not server dependent... very odd.

4 Comments

Hi Ben,

Yeah it's weird.. But on 2000, it's actualy not page not found, but "Method not allowed" (it's 405 - i wrote 403 in the previous post).

According to Adobe, there is 3 possible reasons, and those 2 are interesting...

- "The action attribute of a form does not point to an executable script or there is no file specified for the action (in which case, the action field in the Property inspector is blank)."

- "The form's action attribute points to an HTML file."

Maybe it's the second reason. So maybe it not allowed to post a form on a htm file on 2000, but allowed on 2003. And maybe 404 doesn't not pass form value and 405 does?

So if it was possible to make sure a 405 is flaged instead of 404 it would maybe work.! But i'm not sure it's possible to do that...

15,640 Comments

@Etienne,

Interesting stuff. Yeah, I generally point my form actions to CFM pages. I figure form processing pages don't need to be search engine friendly.

4 Comments

Hi Ben,

thanks for the brainstorm today.!

I've found it not possible anymore in 2003.

http://www.hostingforum.ca/3380-iis-6-form-post-data-missing-404-405-custom-error-handle.html

So I found a way to post forms on existing page instead.

ciao!

50 Comments

To Dave and others who like the idea of instead just putting a CFML-based error handler into the IIS custom error handler for 404's, beware. That now takes control for filetypes other than just .cfm,meaning GIFs, JPGs, etc.. I saw one situation where a site had lots of missing files, and they used this technique to have the IIS 404 call a CFM, which did a CFLOCATION to a CFM page.

Well, under load, that was killer, because the missing file might just be a spacer.gif being loaded up in many places on a page. Multiple that by high load, and suddenly every page request (with img srcs tags trying to get this missing GIF) would now trigger the CFM error handler page, which in their case then redirected to a front page (also a CFM page). Since the spacers were set to 1x1, no one ever noticed this happening. Yikes.

Clearly, such an IIS-based custom 404 error handler written in CFML needs to to either be coded to handle only CFM, CFML, or CFC files, or you need to consider some other solution (such as Ben's original one). Dave's was a short-cut. Ben didn't have the IIS 404 error handler pointing to a CFM page.

One might propose to instead assign Dave's 404handler.cfm to the CF Admin "site-wide missing template handler", where at least it will apply only to files that are handed to CF by the web server. And in CF8, we also now have the new per-application missing template handler (where you can define an onMissingTemplate function in application.cfc).

Hope that helps someone.

15,640 Comments

Charlie makes a good point. I have seen sites where people were including old CSS sheets and JS files that no longer existed and there were throwing "hidden" 404 errors on every single page request.

One thing you can be sure to do is that if you handle a 404 and decide that it's not a "Valid" 404, then log it in some way that you can see the requests that are triggering the error.

1 Comments

Some background:
My 404 page contains cfincludes for header/footer files and also includes loads of variables/structures that are initialized in application.cfm.

This wouldn't regularly be an issue if IIS had "Check if files exists" on. But I've got "Check if file exists" in IIS off due to seo friendly urls. So any non .cfm/.cfml page that doesn't exist get's handed off perfectly by IIS/Coldfusion. But, if a .cfm/.cfml page doesn't exist for some odd reason the application.cfm page IS NOT INCLUDED.

The Workaround:
I had to add the following workaround to the very top of the 404.cfm page, it doesn't include application.cfm if it's called directly because it loads correctly, if it's called by coldfusion's missing template handler it does include it:

<cfif cgi.script_name neq "/404.cfm">
<cfinclude template="/application.cfm">
</cfif>

Where "/404.cfm" is your missing template handler. Maybe I'm missing something? or is this a bug? Either way I hope this helps someone.

3 Comments

So clarify please... should I or should I not be using this example as my IIS 404 error handler? Is this page going to be hit for every 404 including missing images, etc.?

15,640 Comments

@Connie,

I use a newer version of this technique for this blog and it has been working fantastically. I'm not quite happy with the implementation; but aside from some clean up, I've been very happy with this.

15,640 Comments

@Connie,

Also, yes, this will be hit when images are missing as well. But, you can put logic in to send that to a standard 404.

1 Comments

Has anyone come up with a fix for the problem with IIS under 2003 not including the form variables with the 404 redirect? This has been a problem for me since IIS 5. They do not seem to get passed to my custom error handler nor do they appear in any of the other CGI variables.

74 Comments

Ben,

Do you have your "newer" version blogged about somewhere? Read your reply two-up. I'm looking for a working 404 handler for cfms and cfcs. I know you are onto modrewrite now, but we don't have that available.

Thanks!

15,640 Comments

@Justin,

I've actually starting using a tiny bit of URL rewriting with IIS Mod-Rewrite. It's more or less the same kind of logic, but 404 errors get re-written to point to the front-controller (index.cfm) where the 404 can be full processed:

www.bennadel.com/blog/1696-Exploring-IIS-Mod-Rewrite-For-Rewriting-URLs-In-A-ColdFusion-Application.htm

... and here:

www.bennadel.com/blog/1701-Using-IIS-URL-Rewriting-And-Application-cfc-s-OnMissingTemplate-Event-Handler.htm

... and here:

www.bennadel.com/blog/1744-Using-IIS-URL-Rewriting-And-CGI-PATH_INFO-With-IIS-MOD-Rewrite.htm

The logic that I am using in production is more or less along these various lines. Basically, in the URL rewriting, I append the 404 url as a query string variable during the rewrite. Then, in the onRequestStart() event handler, I check the 404 query string variable for patterns and route as appropriate.

74 Comments

Ben,

I read all those posts looking for an update. So, I just wanted to make sure that your comment above wasn't implying that you significantly changed this code for the better. By the way, the snippet doesn't work.

Cheers

74 Comments

Just to be clear-clear. I don't need further support on this issue. However, the snippet link above doesn't work. It just keeps telling me to get back to the gym. However, it's so hard now with a kid and a house and a new hobby or two...and...and...and...

15,640 Comments

@Justin,

Ha ha, I had not idea what you were talking about at first :) I haven't use the "Snippets" section in so long. I disables that part of the site a while back and totally forgot about it. I'll have to update the link.

As far as the error handling, yeah, the premise is almost exactly the same. The way I do it now is I rewrite 404 requests to something like

./index.cfm?404=(...original_request...)

Not exactly that, but that's the idea. Then, in my Application.cfc, I run pattern matching against the original request to figure out how I want to route it.

Same concept, but I think it's a bit cleaner.

17 Comments

As always, Ben, great post! - Based on your example, I've been using something similar on our production servers for quite some time now.

Here goes my question for today :
- Does anyone know how to set up something similar on CF's built in web server in order to emulate the 'SEO friendly' url on a development environment ?

I've been looking into the SERVER-INF/default-web.xml config file :
<error-page>
<exception-type>java.io.FileNotFoundException</exception-type>
<location>/Default.cfm</location>
</error-page>

...with unexpected results, so far.

17 Comments

<error-page>
<exception-type>java.io.FileNotFoundException</exception-type>
<location>/</location>
</error-page>

...in SERVER-INF/default-web.xml _seems_ to work !

2 Comments

I'm still having an issue getting this 404 thing to work.

I've setup IIS 7 to use URL:
/errors/404-test.cfm as the 404 page handler.

This works great if someone were to request:
www.mitchellcc.edu/about.html (or .htm)

But our site has always been CF, and when you request:
www.mitchellcc.edu/about.cfm

You get the standard CF "File Not Found" template.

What am I missing here?

P.S. I can't use the site-wide handlers, because I have multiple sites with different designs on the same server using sub-domains.

Thanks.

17 Comments

@David,

That's a normal CF behavior, you should set up an onMissingTemplate function in your application.cfc.

2 Comments

@Gov, What if I'm still using <cringe> Application.cfm?

I've got a CFERROR tag for Requests and Exceptions. I thought the Request one would capture missing pages/templates?

17 Comments

@David,

I was afraid you ask that question. I have no idea; may be we should ask Ben!

Ben! Where Art Thou? Please help!

1 Comments

Hi there

I have a simple CF page set to do a dump of the server only at this stage.

<cfdump var="#server#">

It works fine when I'm on the server itself and run from 127.0.0.1/project-tracker/index.cfm

When I try to run from www.project-tracker.com.au - I get a 404 file or directory not found

Is this a CF error or is this an error being thrown up from IIS

Thanks in advance.

Kind regards

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel