Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with:

Using IIS URL Rewriting And Application.cfc's OnMissingTemplate() Event Handler

By Ben Nadel on

When I first starting looking into IIS URL rewriting using IIS Mod-Rewrite, I stated that I didn't want to start distributing the business logic of my application too widely. As such, all my URL rewriting configuration file did was reroute missing-file requests to my ColdFusion application's front-controller. This way, all of the URL mapping was handled directly by my ColdFusion application, which is where it was most relevant.

In this post, I want to follow that same philosophy; but, rather than redirecting all missing-file requests to the front-controller, I want to leverage the OnMissingTemplate() event handler already available in the ColdFusion application framework. One of the biggest short-comings of the OnMissingTemplate() event handler is that it only works with ColdFusion-based requests (CFM and CFC files). To have non-ColdFusion page requests hook into this event, my IIS URL rewriting file will simply append "-.cfm" to any missing-file request that is not a ColdFusion template:

  • # IIS Mod-Rewrite configuration file
  • # Turn on the rewrite rules for this access file. This will
  • # will handle all requests based off of this directory.
  •  
  • RewriteEngine On
  •  
  •  
  • # If the given file ends in .CFM or .CFC then just let the
  • # request carry on to the file system as it will be handled by
  • # the ColdFusion application framework (which is ultimately what
  • # we want it to be doing).
  • #
  • # NOTE: We don't care if the target CFM or CFC exists as this is
  • # going to be handled at the ColdFusion application framework
  • # level with the OnMissingTemplate() event handler.
  •  
  • RewriteRule \.c(fm|fc)$ - [NC,L]
  •  
  •  
  • # If the given file or directory exits, then don't do any
  • # redirects - simply pass the request on to the file system.
  •  
  • RewriteCond %{REQUEST_FILENAME} -f [OR]
  • RewriteCond %{REQUEST_FILENAME} -d
  • RewriteRule .? - [L]
  •  
  •  
  • # If we have made it this far, then we know that the user
  • # requested a file or directory that didn't exist AND that file
  • # was not a ColdFusion file. As such, all we want to do is
  • # slap a .CFM file extension on this request so that it will
  • # end up triggering the ColdFusion application framework.
  • #
  • # NOTE: This will cause an internal redirect to a file ending
  • # in "-.cfm", which will be immediately passed through (to the
  • # ColdFusion application framework) by the first rule in this
  • # configuration file.
  •  
  • RewriteRule ^(.*)$ $1-.cfm [L]

This IIS Mod-Rewrite configuration file has three different rules. Let's take a look at each in order.

  • RewriteRule \.c(fm|fc)$ - [NC,L]

This rule states that if the requested URI ends in either "CFM" or "CFC", let it pass through - no internal redirect (rewriting) needs to take place. The [NC] flag is for case-insensitivity and the [L] flag means that this is the Last rule that should be evaluated on this pass. There is no need for the [QSA] (Query String Append) flag because no rewrite is taking place; as such, any URL variables will automatically be passed through. The dash ("-") after our regular expression pattern signifies that no rewriting will take place on requests that get matched. The bottom line is that if the request is a ColdFusion-based request, simply let it pass through to the underlying ColdFusion application without any further processing.

  • RewriteCond %{REQUEST_FILENAME} -f [OR]
  • RewriteCond %{REQUEST_FILENAME} -d
  • RewriteRule .? - [L]

This rule matches any request (with or without a URI length) and checks to see if the requested file is an existing file (-f) or an existing directory (-d). If the request has an existing corresponding file item, the dash ("-"), after our regular expression pattern, signifies that no rewriting will take place. Again, the [L] flag means that this is the Last rule that should be evaluated on this pass. The bottom line here is that if the request is valid, simply let it pass through to the underlying application (whether that is ColdFusion, or just an IIS file serve).

NOTE: We didn't check for an existing file (-f) or an existing directory (-d) in the first rule because it didn't matter; in fact, the whole point of this post is to let ColdFusion handle "missing template" errors internally. As such, existing of the request ColdFusion template is not and should not be a concern of our IIS Mod-Rewrite configuration file.

  • RewriteRule ^(.*)$ $1-.cfm [L]

By the time we get to this third rule, we know two solid facts: 1) The requested file was not a ColdFusion file (rule 1) and 2) The requested file did not exist (rule 2). As such, we know that this request does not directly correspond to any file on the server. Rather than letting the server throw a 404 (File Not Found) error, we want to leverage the existing OnMissingTemplate() feature of our ColdFusion application framework such that this 404 can be handled by internal and not external business logic. However, since we know that this request is not a ColdFusion-based request, we need to rewrite it such that it becomes a ColdFusion-based request.

And, that's what this rule does - it takes the current requests and appends "-.cfm" to it such that it is now a request for a ColdFusion template. This rewrite causes an internal redirect and this configuration file is then re-run using the new, cfm-suffixed URI. At this point, the first rule in our IIS Mod-Rewrite file matches the ".cfm" template request and allows the request to pass through to the underlying ColdFusion application.

One final thing to note about this RewriteRule is that I am using the "-" in "-.cfm" for a very specific reason: it will match a word boundary (\b) in any pattern matching that we do on the request internally to the ColdFusion application. Had I used an "_" or any other word character, this would not be the case.

Now that we have our IIS URL rewriting configuration file and understand what it's doing, let's take a look at our ColdFusion Application.cfc framework file. Unlike my first post on this URL rewriting concept, this version will actually be triggering OnMissingTemplate() events. As such, our request methods (OnRequestStart() and OnRequest()) will not be called implicitly, but rather, explicitly by our OnMissingTemplate() event handler. When looking at the following code, notice that our OnMissingTemplate() event handler passes an optional flag to the OnRequestStart() event handler to signify that the incoming URL is an SEO (Search Engine Optimized) request.

Application.cfc

  • <cfcomponent
  • output="false"
  • hint="I define the application settings and event handlers.">
  •  
  • <!--- Define the application. --->
  • <cfset this.name = hash( getCurrentTemplatePath() ) />
  • <cfset this.applicationTimeout = createTimeSpan( 0, 0, 5, 0 ) />
  •  
  • <!--- Define page request settings. --->
  • <cfsetting
  • requesttimeout="10"
  • showdebugoutput="false"
  • />
  •  
  •  
  • <cffunction
  • name="onApplicationStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I initialize the application.">
  •  
  • <!--- Define the local scope. --->
  • <cfset var local = {} />
  •  
  • <!---
  • As part of the application initialization, we
  • want to figure out some constants surrounding our
  • application location:
  •  
  • RootDirectory
  • The root directory of our application.
  •  
  • RootScript
  • The root script path of our application. This is in
  • the case where our application lives below the web
  • root of the server.
  •  
  • RootUrl
  • The root URL of our application.
  • --->
  •  
  • <!---
  • Determining the root path is easy - we always know
  • that it is this directory (the one containing the
  • Application.cfc component).
  • --->
  • <cfset application.rootDirectory = getDirectoryFromPath(
  • getCurrentTemplatePath()
  • ) />
  •  
  • <!---
  • To find the Root Script, we have to do a bit more
  • calculation; we need to figure out the difference
  • in the length between the root directory and
  • requested directory and then subtract that depth
  • from the requested script.
  • --->
  •  
  • <!---
  • Start off with the current script directory as the
  • root directory.
  • --->
  • <cfset application.rootScript = getDirectoryFromPath(
  • cgi.script_name
  • ) />
  •  
  • <!---
  • Comparing the expanded root script to the root
  • directory, we can now figure out how many directories
  • below the application root we are.
  • --->
  • <cfset local.scriptDepth = (
  • listLen( expandPath( application.rootScript ), "\/" ) -
  • listLen( application.rootDirectory, "\/" )
  • ) />
  •  
  • <!---
  • Based on the script depth, we can now move up the path
  • the corresponding number of steps.
  • --->
  • <cfset application.rootScript = reReplace(
  • application.rootScript,
  • "([^\\/]+[\\/]){#local.scriptDepth#}$",
  • "",
  • "one"
  • ) />
  •  
  • <!---
  • Now that we have our root script, we can easily find
  • our root URL. The only special case we need to worry
  • about is when the root script is "/". In that case,
  • we are in the root of the web directory and don't need
  • to append the script.
  • --->
  • <cfset application.rootUrl = (
  • "http://" &
  • cgi.server_name
  • ) />
  •  
  • <!---
  • Check to see if we have a script name worth appending
  • to the URL.
  • --->
  • <cfif !reFind( "^[\\/]$", application.rootScript )>
  •  
  • <!--- Append root script to URL. --->
  • <cfset application.rootUrl &= application.rootScript />
  •  
  • </cfif>
  •  
  • <!--- Return true so the page request can process. --->
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onRequestStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I intialize the page request.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="script"
  • type="string"
  • required="true"
  • hint="I am the requested script name."
  • />
  •  
  • <cfargument
  • name="isSEO"
  • type="boolean"
  • required="false"
  • default="false"
  • hint="I flag whether or not the requested script was a SEO (search engine optimized) url. This will only be true if this event is being called by the OnMissingTemplate() event handler."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset local = {} />
  •  
  • <!---
  • Combine the form and url scopes into a common request
  • attributes collection so that we don't have to know
  • what scope a variable came from.
  • --->
  • <cfset request.attributes = duplicate( url ) />
  • <cfset structAppend( request.attributes, form ) />
  •  
  • <!---
  • Param the default action variable - this will be
  • what the front-controller (and sub-controllers) use
  • to figure out what scripts to execute.
  • --->
  • <cfparam
  • name="request.attributes.do"
  • type="string"
  • default=""
  • />
  •  
  • <!---
  • Split the request variable into an array such that
  • we can examine the parts of it in front-controller
  • control flow. We are going to assume that the raw
  • action variable is a dot-delimmited list of actions.
  • --->
  • <cfset request.do = listToArray(
  • request.attributes.do,
  • "."
  • ) />
  •  
  •  
  • <!---
  • Now that we have our action variable set up and based
  • of the query string, let's check to see if the current
  • page request is actually a URL Rewriting (an SEO url
  • that was caught by the OnMissingTemplaet() event
  • hanlder). If so, we might have to translate the URL
  • into a new action AND a set of query string parameters.
  •  
  • NOTE: Our IIS MOD-Rewrite interceptor rewrite rule
  • will simply append "_.cfm" to any non-existing URL
  • that doesn't already end in "cfm".
  • --->
  • <cfif arguments.isSEO>
  •  
  • <!---
  • Since the request script is based on the web root
  • and NOT on our application root, let's remove
  • anything above our application that will make our
  • pattern matching more difficult. We can do this
  • be substracting the root script from the user-
  • requested script.
  •  
  • This will leave us with application-relevant
  • script values like:
  •  
  • subdirectory/file.cfm
  •  
  • Now, our patterns only have to worry about matching
  • from the application-down, and not worry about the
  • greater location of our application.
  • --->
  • <cfset local.script = replace(
  • arguments.script,
  • application.rootScript,
  • "",
  • "one"
  • ) />
  •  
  • <!---
  • Now that we have our script name calculated, let's
  • use some regular expression pattern matching to
  • see if we need to update our action varaible or
  • any other URL variables.
  • --->
  • <cfif reFind( "^contact\b", local.script )>
  •  
  • <!--- Routing to contact section. --->
  • <cfset request.do = [ "contact" ] />
  •  
  • <cfelseif reFind( "^about\b", local.script )>
  •  
  • <!--- Routing to about section. --->
  • <cfset request.do = [ "about" ] />
  •  
  • <cfelseif reFind( "^blog/[\d+]", local.script )>
  •  
  • <!--- Routing to blog section. --->
  • <cfset request.do = [ "blog" ] />
  •  
  • <!--- Get ID of blog post. --->
  • <cfset request.attributes.id = listGetAt(
  • local.script,
  • 2,
  • "/"
  • ) />
  •  
  • <cfelseif reFind( "^blog\b", local.script )>
  •  
  • <!--- Routing to blog section. --->
  • <cfset request.do = [ "blog" ] />
  •  
  • <cfelse>
  •  
  • <!---
  • We could not match the requested URL against
  • any of our SES patterns. As such, this is
  • truly an invalid file request. As such, let's
  • return a true 404 error.
  • --->
  • <cfheader
  • statuscode="404"
  • statustext="Page Not Found"
  • />
  •  
  • <!---
  • Return out with false so the request of the
  • page will not get processed.
  • --->
  • <cfreturn false />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Get the relative web root path from our current page
  • (this will allow our traversal path to always be
  • relative, rather than a hard-coded root path, which
  • is the lame).
  •  
  • NOTE: Even if we end up including our front-controller,
  • from the client (browser) perspective, the URL has not
  • change. As such, our web root value must be calculated
  • based on THAT url, not the template that we eventually
  • end up executing.
  • --->
  • <cfset request.webRoot = repeatString(
  • "../",
  • (
  • listLen( getDirectoryFromPath( expandPath( arguments.script ) ), "\\/" ) -
  • listLen( application.rootDirectory, "\\/" )
  • )) />
  •  
  •  
  • <!--- Return true so that the page can be processed. --->
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onRequest"
  • access="public"
  • returntype="void"
  • output="true"
  • hint="I execute the page request.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="script"
  • type="string"
  • required="true"
  • hint="I am the requested script name."
  • />
  •  
  • <!---
  • Include the requested page. If the person requested
  • a valid CFM page, then include it.
  •  
  • NOTE: If this event is getting called from the
  • OnMissingTempalte() event handler, then the script
  • being passed in MAY NOT be the same script requested
  • by the user.
  • --->
  • <cfinclude template="#arguments.script#" />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onMissingTemplate"
  • access="public"
  • returntype="boolean"
  • output="true"
  • hint="I execute if the requested template does not exist.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="script"
  • type="string"
  • required="true"
  • hint="I am the requested script name (but I do not exist on the physical file system)."
  • />
  •  
  • <!---
  • Since the OnMissingTemplate() event does not
  • implicitly trigger the "request" events, we have to
  • calls those explicitly. To stay in alignment with
  • the intent of the OnRequestStart() event handler,
  • let's call it as a part of a conditional.
  •  
  • NOTE: The second argument, which is not normally
  • supplied by the ColdFusion application framework, is
  • a flag to the request method signaling that the
  • requested script did NOT exist (such that any special
  • processing can take place).
  • --->
  • <cfif this.onRequestStart( arguments.script, true )>
  •  
  • <!---
  • Explicitly execute the OnRequest() event handler.
  • However, since we know that the requested script
  • does NOT exist, we are going to override the
  • requested script with the front-controller.
  • --->
  • <cfset this.onRequest( "index.cfm" ) />
  •  
  • <!---
  • Return true since the request executed
  • successfully. This will prevent a 404 error from
  • being thrown.
  • --->
  • <cfreturn true />
  •  
  • <cfelse>
  •  
  • <!---
  • Since the onRequestStart() did not like the
  • script name that was called, let's return false.
  • This will trigger the actual "File Not Found"
  • error at the ColdFusion level.
  • --->
  • <cfreturn false />
  •  
  • </cfif>
  • </cffunction>
  •  
  • </cfcomponent>

Because the ColdFusion application framework does not implicitly call both the request event handlers and the OnMissingTemplate() event handler, we have to call the request methods directly from our OnMissingTemplate() event handler. When doing this, we want to pass the requested URL to the OnRequestStart() event method such that it can run pattern matching on the request; however, when we call the OnRequest() event handler, we need to override the requested URL with our front-controller (index.cfm) since any attempt to CFInclude the requested - but non-existing - template will throw an exception.

In the context of the ColdFusion application framework, the OnRequestStart() and the OnMissingTemplate() event handlers both have very meaningful return values. If the OnRequestStart() method returns FALSE, it signifies that the request should not be processed. Furthermore, if the OnMissingTemplate() method returns FALSE, it signifies that the request was truly a 404 (File Not Found) error. In order for these two methods to work meaningfully together, we have to honor the return values of both methods and let one influence the other. That's why we are including the explicit OnRequestStart() method call as part of a CFIF statement.

The rest of the Application.cfc is fairly straightforward - the OnRequestStart() event handler checks to see if the request is an SEO request (as defined by the optional argument passed in by the OnMissingTemplate() event) and if so, it see if it can match it against a series of regular expressions. If any of the regular expressions do match, the action variable and any appropriate request variables are overridden.

The front-controller (index.cfm) in this application simply outputs a bunch of scopes such that we can see how the URL rewriting is being handled:

Index.cfm

  • <cfoutput>
  •  
  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>IIS Mod-Rewrite ColdFusion Demo Application</title>
  • </head>
  • <body>
  •  
  • <h1>
  • IIS Mod-Rewrite ColdFusion Demo Application
  • </h1>
  •  
  • <h2>
  • Test Links
  • </h2>
  •  
  • <p>
  • <a href="#request.webRoot#contact/"
  • >#request.webRoot#contact/</a><br />
  •  
  • <a href="#request.webRoot#about/"
  • >#request.webRoot#about/</a><br />
  •  
  • <a href="#request.webRoot#blog/"
  • >#request.webRoot#blog/</a><br />
  •  
  • <a href="#request.webRoot#blog/1234/"
  • >#request.webRoot#blog/1234/</a><br />
  • </p>
  •  
  • <h2>
  • Relative Web Root
  • </h2>
  •  
  • <p>
  • <cfif len( request.webRoot )>
  • #request.webRoot#<br />
  • <cfelse>
  • [empty string]
  • </cfif>
  • </p>
  •  
  • <h2>
  • URL Scope
  • </h2>
  •  
  • <cfdump
  • var="#url#"
  • label="url"
  • />
  •  
  • <h2>
  • REQUEST Scope
  • </h2>
  •  
  • <cfdump
  • var="#request#"
  • label="request"
  • />
  •  
  • <h2>
  • APPLICATION Scope
  • </h2>
  •  
  • <cfdump
  • var="#application#"
  • label="application"
  • />
  •  
  • </body>
  • </html>
  •  
  • </cfoutput>

To demonstrate how the rewritten URL can be translated into both action variables as well as URL/Request variables, I am going to go to the URL:

./blog/1234/

When I hit this URL (which doesn't exist), the IIS Mod-Rewrite configuration file redirects it internally to this URL:

./blog/1234/-.cfm

Now that it has a "cfm" file extension, it can be handled internally to the ColdFusion application framework. This new, non-existing URL gets caught by the Application.cfc's OnMissingTemplate() event handler, which executes our front-controller, and outputs the following:

 
 
 
 
 
 
Using IIS Mod-Rewrite To Perform IIS URL Rewriting To Hook Into The OnMissingTemplate() Event Handler In The ColdFusion Application Framework. 
 
 
 

Notice that the requested URL was translated into the action variable [ "blog" ] and the ID "1234". Notice also that the relative web root was correctly defined as "../../" such that none of our linked files (CSS, Image, Script, etc.) will break.

As I stated in my earlier blog post, there is definitely some efficiency to handling more of the business logic at the IIS level; however, something about that just feels wrong. Maybe it's because I am new to IIS URL rewriting, but I feel that as much as the control flow of the ColdFusion application should be located in the actual ColdFusion application layer. As such, I really like the idea of using IIS Mod-Rewrite simply as a means to hook into the OnMissingTemplate() event handler; this makes up for the critical gap in the ColdFusion application framework functionality, but just enough to empower it rather than to undermine it.




Reader Comments

@Ben,

You seem to think that the solution described here is technically or aesthetically better than the solution of simply rewriting via script_name -> path_info (both CGI/Environment variables with defined semantics).

With the solution described here, you are actually modifying the script_name.

Why rewrite "/a/b/c" to "/a/b/c/-.cfm", rather than simply rewrite "/a/b/c" to "/index.cfm/a/b/c" (the latter preserves all path information unaltered, which can be retrieved from CGI.PATH_INFO).

You can use either Application.cfc#onMissingTemplate or an actual index.cfm file with this solution (depending on whether you actually have an index.cfm file).

The solution I described is fairly standard across languages and framework that do not directly support full url-routing (eg script-file-based languages), such as various PHP frameworks and various ColdFusion frameworks. For example, ColdCourse uses this approach.

Justice

Reply to this Comment

@Justice,

What you are saying is very interesting. Since I have not used path_info much at all in standard requests, it never occurred to me to use it as part of the internal redirect. But, it is very interesting (and, easily accessible).

Let's make no mistake, however - in both of the cases (yours and mine), we are actually changing the script name. That is - if you can think about it that way as after all, the requested script name does not exist in either case. So, both techniques change the script name from a non-existent one to another one.

I wonder, however, if using the front-controller / path_info combination actually makes more implicit assumptions about the underlying application. Take, for example, the following application architecture:

./Application.cfc
./index.cfm
./sub/
./sub/Application.cfc
./sub/index.cfm

If I were to make a request to the non-existent directory:

./sub/contact/

... my version would simply rewrite the url to be:

./sub/contact/-.cfm

... which would trigger the OnMissingTemplate() in the sub directory's Application.cfc (which might presumably extend the root Application.cfc).

If, however, this simply rerouted to the front-controller with path information:

./index.cfm/sub/contact/

... this would trigger the OnMissingTemplate() in the root Application.cfc, and not the sub directory's Application.cfc.

Is this a bad thing? Not inherently, but it might be. We could, of course, start putting more rules in the URL Rewrite config file to check for various sub-directories and reroute to the more appropriate front-controller (root or sub-directory)... but at that point, our URL Rewrite file is required to know more about underlying application that I might want it to.

By simply changing the requested URL to be a ColdFusion-based URL, we can let the underlying application architecture remain as decoupled as possible (the only thing need knowing is the fact that the underlying application IS a ColdFusion application).

Essentially, what I like about my approach is that is acts as the most minimal bridge between the request and the ColdFusion application.

Of course, that said, I am VERY new to all of this URL rewriting stuff, so I have no understanding of what "best practices" are out there.

Reply to this Comment

@Ben,

You are right that the web server should be very dumb w.r.t. the structure of the application.

I am very much in the camp that says there should be only one Application.cfc per application. No tolerance for subdirectory Application.cfc's, unless that subdirectory actually is its own application - in which case, the web server config *should* know about it, and the two applications should *not* be sharing configs and variables etc.

So moving the script_name to path_info, and replacing script_name with '/index.cfm' (or '/nonexistent.cfm') works very well, and it turns out that the behavior is what I actually want to happen. (Yes, the script_name variable takes on a new value; but the original value remains preserved in a new variable, path_info, which is fine by me.)

I do not have a laundry list of rules in my applications' front controllers. I auto-discover the functionality available in the application (easy: cfdirectory over /app/components/controllers/*Controller.cfc) and then figure out if the path_info matches a valid controller and action that have been auto-discovered. So adding new legitimate paths doesn't require I muck around with the Application.cfc all the time, only that the cfapplication is restarted. The application architecture is very decoupled, which is the goal you are after as well. It does all of the dispatching (rather than the web server doing the dispatching) but requires almost no configuration to do so.

I also generate the URLs just like one would do in Rails or Django - something like <cfoutput>#url_for(controller="user_account", action="index")#<cfoutput> will automatically generate '/user/account' - or, if this app is hosted in a virtual directory or alias, it will automatically generate '/alias/subalias/user/account'.

And, best of all, with a single switch in Application.cfc and commenting out one rule in .htaccess or Web.config, I can go from using actual URLs, the ones appearing in the browser, like '/user/account' to '/index.cfm/user/account' and back (or '/alias/subalias/user/account' to '/alias/subalias/index.cfm/user/account').

Justice

Reply to this Comment

@Justice,

I agree with you - I've never really had a need for a sub-Application.cfc; I was just throwing it out there as a stream of consciousness. In fact, I have often found that the sub-Application.cfc's tend to cause a lot of headache. Or maybe I just don't have a good use case.

And, like you, my intent would also be to just include the front-controller. In fact, if you look at my sample code, the OnMissingTemplate() does override the script name to include the front-controller.

As far as the patterns being located in Application.cfc itself, I probably wouldn't do that. I just did that here for ease of demo. Traditionally, I'll distribute that among the controllers. Not in the best way - I usually have some sort of "seo" type file in each controller that gets compiled by a master pattern matcher. Something I've wanted to rework for a long time, but never had time for.

I haven't done any kind of auto-generating of URLs like what you are saying. I think that works when you have a very strict link between the URL and the variables. Does that hold up when you have arbitrary translations? I suppose you could always write your some sort of extension to whatever you have that builds custom URLs.

For example, I might have a "friendly" URL that is:

site.com/go/ben

... which translates to:

site.com/index.cfm?action=shortcut&q=ben

In this case, there is not assumed connection between "go" and the action "shortcut".

Of course, now we have gone way off topic, but I see what you're saying. I think we are mostly on the same page; I'm just exploring various ways of carrying this out. Perhaps, next, I'll try a version that uses your path_info, just see how it feels.

Reply to this Comment

@Ben,

In a URL there are multiple components, but I will divide them into three:
- the application: http://example.com/myapp/mysubapp
- the resource: /admin/customers/334
- parameters: ?include_orders=yes&include_pending_shipments=yes

The resource part should contains all the information necessary to identify what we want to view. Luckily, that is typically not too complex. The parameters part includes anything that might modify how that what is represented or displayed, so that part should not be in the path - it should only be in the query-string.

This does require standardizing on urls that look a certain way. And for complex url structures you can certainly have a central configuration where you define the minimal things necessary to set up the url structure. So you would have to assume connections - or configure them.

But that's OK, because the concept is called Convention Over Configuration.

Justice

Reply to this Comment

@Justice,

I understand the concepts behind the URL; but, sometimes, the resource is not so cleanly converted. For example, if you look at my blog, I might have a blog post that is such:

bennadel.com/blog/1234-hello-world.htm

Underneath, this get's translated to the URL:

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

In my case, I have to understand that the both the "ID" and the blog post title get joined to create the simulated file name for the blog entry. This is / can be a completely arbitrary translation.

But like you're saying, there is convention and then there is configuration. In my case, I simply have to "configure" my URLs as the URLs cannot be created based on convention.

I guess it just depends on what you are trying to do.

I think the ultimate form of convention would a REST-style interface where the URL is composed of actions and variables (which I think is what you are leaning more towards, correct?).

Reply to this Comment

@Ben,

Actions are a feature of MVC.

HTTP methods (GET, POST, PUT, DELETE) are features of REST. Unfortunately, REST often has to be simulated by MVC actions or form-parameters because browser support for REST is broken at best and non-existent in general.

I do like where Rails is going: REST.

For your blog url, I would handle it with the controller /app/controllers/blog.cfc and method show(id). The url-routing would need to be configured a little bit extra to append the title to the generated url and strip the title out of incoming HTTP-Request urls.

Convention exists to support the default or basic cases, not the complicated cases. This particular case is more complicated than most, so it requires configuration. But that is all wrapped up in the concept Convention Over Configuration. All the basic things should work out of the box, and anything more complex should work with just a bit of tweaking.

Justice

Reply to this Comment

@Justice,

I am sure as I start to play more with ColdBox, I'll be getting more into convention as the whole framework requires it.

Also, when I get back to the office (I'm at jQuery conference 2009), I want to play around with using the path_info as you suggested.

Reply to this Comment

Ben --

Have you had any experience with the IIS ISAPI_REWRITE module and its interactions with Adobe Contribute?

Everytime I enable an .htaccess file with URL rewriting at the root level, Contribute crashes for all users. I'm not even doing anything all that fancy -- just snagging -f/-d URLs and passing them to our 404 helper page.

Here's what I have:
AddType text/html .dwt
AddType application/octet-stream .doc .xls .docx .xlsx .ppt .pptx

RewriteEngine on

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^.*\.(jpg|gif|png)$ /pub/img/icons/x.gif [L]

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule (.*) /sitemap.php?404=true&lookingfor=%{REQUEST_URI} [NC,L]

I've asked Adobe about this but they're silent. It's my thought that while Contribute uses FTP to upload, it uses HTTP to get the templates or other data. I think there's something in there with the temp directory / file creation as well.

Any experience with it? I'd really like to get this working, and as bad as Contribute is, it would be nice if we could keep using it.

Reply to this Comment

@Aaron,

I am relatively new to URL Rewriting in general, but I cannot see anything in your rules that would cause errors. Are you logging 404 errors? Can you assert that the Contribute is actually being rerouted to your 404 page?

Reply to this Comment

Sorry, I should have been more specific.

Contribute isn't 404'ing, it's crashing entirely. Contribute opens, and then when it gets to the "synchronizing templates" step, it completely freezes.

If it was *just* using FTP, this wouldn't happen, as FTP is agnostic to .htaccess rules, so I'm guessing Contribute is making HTTP requests.

Do you guys use Contribute in your shop?

Reply to this Comment

Hi Ben,

I want to set up a rewritten url strategy, using the onMissingTemplate function.

But it seems the "regular" server don't pass the hand to my Coldfusion one.

Am I doing some mistake ?

Here is the concerned code:

The onMissingTemplate function :

  • <cffunction name="onMissingTemplate" returnType="boolean" hint="Fires when an incorrect URL is called">
  • <cfargument type="string" name="targetPage" required="yes">
  •  
  • <!--- check if targetPage's url match the url rewriting's pattern AND rewrite it --->
  • <cfset rewrittenTargetPage = getRewrittenUrl(targetPage)>
  •  
  • <!--- call onRequestStart --->
  • <cfset onRequestStart(rewrittenTargetPage)>
  •  
  • <!--- build and display the default page (home) --->
  • <cfset onRequest(rewrittenTargetPage)>
  •  
  • <!--- call onRequestEnd --->
  • <cfset onRequestEnd(rewrittenTargetPage)>
  •  
  • <cfreturn True>
  • </cffunction>
  •  
  •  
  • The getRewrittenUrl function :
  • <cffunction name="getRewrittenUrl" access="private" returnType="string" output="no" hint="check if targetPage's url match the url rewriting's pattern AND rewrite it">
  • <cfargument name="targetPage" type="String" required="yes" hint="The page asked by the user" />
  •  
  • <!--- check if the url match a pattern --->
  • <cfif REFind("http(s)?//:#CGI.SERVER_NAME#/fashion/jewellery/home.cfm", targetPage)>
  • <cfset rewrittenTargetPage = "/views/home.cfm">
  • <cfelse><!--- No pattern found, url is invalid, return the default page --->
  • <cfset rewrittenTargetPage = "/index.cfm">
  • </cfif>
  •  
  • <!--- return the real (physical) url--->
  • <cfreturn rewrittenTargetPage>
  • </cffunction>

Behaviour :
When I enter on http://ver3.joe-cool.co.uk/index.cfm , no problem, cause the page physically exists.
But when I click on the "Home" link (corresponding to http://ver3.joe-cool.co.uk/fashion/jewellery/home.cfm ) : in place of displaying http://ver3.joe-cool.co.uk/views/home.cfm , it says me :
Not Found | The requested document was not found on this server. | Web Server at joe-cool.co.uk

Do you have an idea ?

Thanks.

PS: Great blog, when I have a complex Coldfusion problem, I usually found my happiness on it. Thanks.

Reply to this Comment

sir can you help me about the url re writing
Now my website url when i clicking on an item is www.sitename.com/viewdetails.aspx"id=134.

I want to change this like ww.sitename.com/134.aspx when clicking on an item. We used many codes using dll but its giving error when working remote..Locally its working well. Our server Windows server2008 R2.

please help me

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.