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 CFUNITED 2009 (Lansdowne, VA) with:

Learning ColdFusion 9: Application.cfc OnCFCRequest Event Handler For CFC Requests

By Ben Nadel on
Tags: ColdFusion

Earlier this week, Ray Camden blogged about ColdFusion 9's new onCFCRequest() event handler method in Application.cfc. If you look at the Application.cfc reference guide in the ColdFusion 9 documentation, you'll notice that it is not there at all (so far the CF9 docs have been extremely under whelming). As such, there are a lot of questions that I have about the use of the OnCFCRequest() event handler and very few answers. So, I figured it was time to take what Ray has shown us and start performing some experiments.

First thing's first, let's just get comfortable with the basic structure of the OnCFCRequest() event handler. As Ray mentioned, OnCFCRequest() takes three arguments:

  • Component: The name/path of the ColdFusion component requested by the user.
  • MethodName: The name of the method requested by the user for execution.
  • MethodArguments: The argument collection (struct) sent by the user in the request.

That's what it takes, but what does it return? From what I can gather, the ReturnType of the OnCFCRequest() method should be VOID just like its OnRequest() counter-part. Returning a value from this method does not seem to play any part in what actually gets returned in the page response. To return a value, you either have to output it within the method body, or stream it back via CFContent (covered later).

Here's what a simple Application.cfc component using OnCFCRequest() might look like:

Application.cfc

  • <cfcomponent
  • output="false"
  • hint="I define the application settings and event handlers.">
  •  
  • <!--- Define the application settings. --->
  • <cfset this.name = hash( getCurrentTemplatePath() ) />
  • <cfset this.applicationTimeout = createTimeSpan( 0, 0, 5, 0 ) />
  •  
  •  
  • <cffunction
  • name="onRequestStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I initialize the request for both CFM and CFC requests.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="template"
  • type="string"
  • required="true"
  • hint="I am the template requested by the user."
  • />
  •  
  • <!--- Set page request settings. --->
  • <cfsetting showdebugoutput="false" />
  •  
  • <!--- Return true so page can run. --->
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onRequest"
  • access="public"
  • returntype="void"
  • output="true"
  • hint="I process the user's CFM request..">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="template"
  • type="string"
  • required="true"
  • hint="I am the template requested by the user."
  • />
  •  
  • <!--- Include the requested page. --->
  • <cfinclude template="#arguments.template#" />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onCFCRequest"
  • access="public"
  • returntype="void"
  • output="true"
  • hint="I process the user's CFC request.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="component"
  • type="string"
  • required="true"
  • hint="I am the component requested by the user."
  • />
  •  
  • <cfargument
  • name="methodName"
  • type="string"
  • required="true"
  • hint="I am the method requested by the user."
  • />
  •  
  • <cfargument
  • name="methodArguments"
  • type="struct"
  • required="true"
  • hint="I am the argument collection sent by the user."
  • />
  •  
  •  
  • <!--- RESPONSE OUTPUT -------------------------- --->
  • <!--- ------------------------------------------ --->
  •  
  • <!--- Output the query string. --->
  • <h3>
  • Query String:<br />
  • #cgi.query_string#
  • </h3>
  •  
  • <!--- Output the URL scope. --->
  • <cfdump
  • var="#url#"
  • label="URL Scope"
  • />
  •  
  • <br />
  •  
  • <!--- Output the onCFCRequest arguments. --->
  • <cfdump
  • var="#arguments#"
  • label="ARGUMENTS Scope"
  • />
  •  
  • <!--- ------------------------------------------ --->
  • <!--- END RESPONSE OUTPUT ---------------------- --->
  •  
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  • </cfcomponent>

Notice that the OnRequest() and the OnCFCRequest() event handlers live side-by-side. We don't need to do anything special in order to get them to play nice. As Ray demonstrated in his blog post, calls made to remote CFCs will simply ignore OnRequest() and trigger OnCFCRequest(). For our initial demonstration purposes, my OnCFCRequest() outputs the query string, URL, and ARGUMENTS Scopes. This way we, we can see how ColdFusion pipes the request into the event handler method.

Now, let's set up a really simple ColdFusion component with a remote access method for testing:

Service.cfc

  • <cfcomponent
  • output="false"
  • hint="I am a service with some remote access.">
  •  
  •  
  • <cffunction
  • name="getGirl"
  • access="remote"
  • returntype="struct"
  • returnformat="json"
  • output="false"
  • hint="I return a girl object.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="name"
  • type="string"
  • required="true"
  • hint="I am the name of the girl being returned."
  • />
  •  
  • <!--- Define the girl object. --->
  • <cfset local.girl = {
  • name = arguments.name
  • } />
  •  
  • <!--- Return girl object. --->
  • <cfreturn local.girl />
  • </cffunction>
  •  
  • </cfcomponent>

All this Service.cfc does is supply a remote method, GetGirl() which takes a girl's name, wraps it up in a struct, and returns it. If called remotely, I have defaulted the ReturnFormat to use JSON conversion.

With our simple Application.cfc / OnCFCRequest() event handler and Service.cfc remote object set up, let's make some page requests. First, I am going to call a simple REST-based request:

Service.cfc?method=getGirl&name=Tricia&returnformat=json

When we run this request, we get the following page output:

 
 
 
 
 
 
ColdFusion 9 OnCFCRequest() Event Handler. 
 
 
 

The first thing we can take away from this is that the call to the actual remote method never executed. The page request was piped into the OnCFCRequest() event handler; but, just as with the OnRequest() event handler, if we don't then explicitly call the requested component and method, it gets ignored. The next thing we can see is that ColdFusion appropriately separates the URL variable from the method arguments. Meaning, it doesn't send things like Method and ReturnFormat as part of the requested method's argument collection.

Seeing this, I wondered what would happen if we passed in "WSDL" with the query string:

Service.cfc?wsdl

Normally, this would present us with the Web Service Definition XML. But, when we run this, we get the following response:

 
 
 
 
 
 
ColdFusion 9 OnCFCRequest() Event Handler. 
 
 
 

It looks like WSDL requests are treated just like any other requests. This does not bode well for the use of OnCFCRequest() in conjunction with SOAP-based web service calls. My guess is that this kind of Application.cfc architecture is not going to play well with any kind of request that uses behind-the-scenes ColdFusion auto-wiring. Things like SOAP requests and AMF requests will probably fail (or be ignored - I am not sure how AMF makes requests).

That said, the rest of this exploration will stick to REST-based requests in which what you see is what you get in the response. Of course, up until now, we really haven't seen any response; or rather, we haven't seen the response expected by the intent of our request URL - all we've seen is the CFDump output we have in our OnCFCRequest() event handler. So, how do take our request, translate it to a CFC method execution, and then return that value?

I actually talked about this in depth when demonstrating how to handle errors in remote API calls in ColdFusion 8. The same principle applies: we need to make the CFC request, check the return format, perform any transformations manually, and then return it. To demonstrate this, I have beefed up the OnCFCRequest() event handler to follow this logic:

OnCFCRequest() Event Handler

  • <cffunction
  • name="onCFCRequest"
  • access="public"
  • returntype="void"
  • output="true"
  • hint="I process the user's CFC request.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="component"
  • type="string"
  • required="true"
  • hint="I am the component requested by the user."
  • />
  •  
  • <cfargument
  • name="methodName"
  • type="string"
  • required="true"
  • hint="I am the method requested by the user."
  • />
  •  
  • <cfargument
  • name="methodArguments"
  • type="struct"
  • required="true"
  • hint="I am the argument collection sent by the user."
  • />
  •  
  • <!---
  • Create the target cfc instance on which we will be
  • performing the remote method call.
  • --->
  • <cfset local.cfc = createObject( "component", arguments.component ) />
  •  
  • <!---
  • Execute the remote method call and store the response
  • (note that if the response is void, it will destroy
  • the return variable).
  • --->
  • <cfinvoke
  • returnvariable="local.result"
  • component="#local.cfc#"
  • method="#arguments.methodName#"
  • argumentcollection="#arguments.methodArguments#"
  • />
  •  
  • <!---
  • Create a default response data variable and mime-type.
  • While all the values returned will be string, the
  • string might represent different data structures.
  • --->
  • <cfset local.responseData = "" />
  • <cfset local.responseMimeType = "text/plain" />
  •  
  • <!---
  • Check to see if the method call above resulted in any
  • return value. If it didn't, then we can just use the
  • default response value and mime type.
  • --->
  • <cfif structKeyExists( local, "result" )>
  •  
  • <!---
  • Check to see what kind of return format we need to
  • use in our transformation. Keep in mind that the
  • URL-based return format takes precedence. As such,
  • we're actually going to PARAM the URL-based format
  • with the default in the function. This will make
  • our logic much easier to follow.
  •  
  • NOTE: This expects the returnFormat to be defined
  • on your CFC - a "best practice" with remote
  • method definitions.
  • --->
  • <cfparam
  • name="url.returnFormat"
  • type="string"
  • default="#getMetaData( local.cfc[ arguments.methodName ] ).returnFormat#"
  • />
  •  
  • <!---
  • Now that we know the URL scope will have the
  • correct format, we can check that exclusively.
  • --->
  • <cfif (url.returnFormat eq "json")>
  •  
  • <!--- Convert the result to json. --->
  • <cfset local.responseData = serializeJSON( local.result ) />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/x-json" />
  •  
  • <cfelseif (url.returnFormat eq "wddx")>
  •  
  • <!--- Convert the result to XML. --->
  • <cfwddx
  • action="cfml2wddx"
  • input="#local.result#"
  • output="local.responseData"
  • />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/xml" />
  •  
  • <cfelse>
  •  
  • <!--- Convert the result to string. --->
  • <cfset local.responseData = local.result />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/plain" />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Now that we have our response data and mime type
  • variables defined, we can stream the response back
  • to the client.
  • --->
  •  
  • <!--- Convert the response to binary. --->
  • <cfset local.binaryResponse = toBinary(
  • toBase64( local.responseData )
  • ) />
  •  
  • <!---
  • Set the content length (to help the client know how
  • much data is coming back).
  • --->
  • <cfheader
  • name="content-length"
  • value="#arrayLen( local.binaryResponse )#"
  • />
  •  
  • <!--- Stream the content. --->
  • <cfcontent
  • type="#local.responseMimeType#"
  • variable="#local.binaryResponse#"
  • />
  •  
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>

As you can see, the OnCFCRequest() event handler takes the request, creates the CFC, calls the method, and then transforms and returns the result using CFContent. Now, when we call this URL:

Service.cfc?method=getGirl&name=Tricia&returnFormat=json

... we get this JSON response:

{"NAME":"Tricia"}

And, when we call this URL:

Service.cfc?method=getGirl&name=Tricia&returnFormat=wddx

... we get this WDDX response:

<wddxPacket version="1.0">
<header/>
<data>
<struct><var name="NAME"><string>Tricia</string></var></struct>
</data>
</wddxPacket>

That seems pretty straight forward, but I know what you're thinking - OK, so we've now re-created the inherent functionality of ColdFusion, but we've added a LOT of overhead. How can that be good?

Excellent question, and one that I certainly asked myself. With the overhead of the manually execution comes some advantages. Namely, better instantiation and caching. Let's take a look at that quickly.

API Component Instantiation And Caching

Because the CFC request is be routed through our OnCFCRequest() event handler, the target ColdFusion component doesn't get instantiated until we create it manually. And, because we are doing manual instantiation, we get two benefits: we can use an Init() method on our API objects to inject any required dependencies (without breaking encapsulation!!) and we can cache the CFC instance so we don't have to create it again.

In the following example, we will caching the target CFC in our APPLICATION. You'll notice that I'm calling Init() on the Service.cfc. I'v added it, but it doesn't do anything so I won't bother showing the code:

Application.cfc

  • <cfcomponent
  • output="false"
  • hint="I define the application settings and event handlers.">
  •  
  • <!--- Define the application settings. --->
  • <cfset this.name = hash( getCurrentTemplatePath() ) />
  • <cfset this.applicationTimeout = createTimeSpan( 0, 0, 2, 0 ) />
  •  
  •  
  • <cffunction
  • name="onApplicationStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I initialize the application.">
  •  
  • <!---
  • Create a cache for our API objects. Each object will
  • be cached at it's name/path such that each remote
  • method call does NOT have to mean a new ColdFusion
  • component instantiation.
  • --->
  • <cfset application.apiCache = {} />
  •  
  • <!--- Return true so page can run. --->
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onRequestStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I initialize the request for both CFM and CFC requests.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="template"
  • type="string"
  • required="true"
  • hint="I am the template requested by the user."
  • />
  •  
  • <!--- Set page request settings. --->
  • <cfsetting showdebugoutput="false" />
  •  
  • <!--- Check for manual application reset. --->
  • <cfif structKeyExists( url, "reset" )>
  • <cfset this.onApplicationStart() />
  • </cfif>
  •  
  • <!--- Return true so page can run. --->
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onRequest"
  • access="public"
  • returntype="void"
  • output="true"
  • hint="I process the user's CFM request..">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="template"
  • type="string"
  • required="true"
  • hint="I am the template requested by the user."
  • />
  •  
  • <!--- Include the requested page. --->
  • <cfinclude template="#arguments.template#" />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onCFCRequest"
  • access="public"
  • returntype="void"
  • output="true"
  • hint="I process the user's CFC request.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="component"
  • type="string"
  • required="true"
  • hint="I am the component requested by the user."
  • />
  •  
  • <cfargument
  • name="methodName"
  • type="string"
  • required="true"
  • hint="I am the method requested by the user."
  • />
  •  
  • <cfargument
  • name="methodArguments"
  • type="struct"
  • required="true"
  • hint="I am the argument collection sent by the user."
  • />
  •  
  • <!---
  • Check to see if the target CFC exists in our cache.
  • If it doesn't then, create it and cached it.
  • --->
  • <cfif !structKeyExists( application.apiCache, arguments.component )>
  •  
  • <!---
  • Create the CFC and cache it via its path in the
  • application cache. This way, it will exist for
  • the life of the application.
  • --->
  • <cfset application.apiCache[ arguments.component ] =
  • createObject(
  • "component",
  • arguments.component
  • ).init()
  • />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • ASSERT: At this point, we know that the target
  • component has been created and cached in the
  • application.
  • --->
  •  
  •  
  • <!--- Get the target component out of the cache. --->
  • <cfset local.cfc = application.apiCache[ arguments.component ] />
  •  
  • <!---
  • Execute the remote method call and store the response
  • (note that if the response is void, it will destroy
  • the return variable).
  • --->
  • <cfinvoke
  • returnvariable="local.result"
  • component="#local.cfc#"
  • method="#arguments.methodName#"
  • argumentcollection="#arguments.methodArguments#"
  • />
  •  
  • <!---
  • Create a default response data variable and mime-type.
  • While all the values returned will be string, the
  • string might represent different data structures.
  • --->
  • <cfset local.responseData = "" />
  • <cfset local.responseMimeType = "text/plain" />
  •  
  • <!---
  • Check to see if the method call above resulted in any
  • return value. If it didn't, then we can just use the
  • default response value and mime type.
  • --->
  • <cfif structKeyExists( local, "result" )>
  •  
  • <!---
  • Check to see what kind of return format we need to
  • use in our transformation. Keep in mind that the
  • URL-based return format takes precedence. As such,
  • we're actually going to PARAM the URL-based format
  • with the default in the function. This will make
  • our logic much easier to follow.
  •  
  • NOTE: This expects the returnFormat to be defined
  • on your CFC - a "best practice" with remote
  • method definitions.
  • --->
  • <cfparam
  • name="url.returnFormat"
  • type="string"
  • default="#getMetaData( local.cfc[ arguments.methodName ] ).returnFormat#"
  • />
  •  
  • <!---
  • Now that we know the URL scope will have the
  • correct format, we can check that exclusively.
  • --->
  • <cfif (url.returnFormat eq "json")>
  •  
  • <!--- Convert the result to json. --->
  • <cfset local.responseData = serializeJSON( local.result ) />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/x-json" />
  •  
  • <cfelseif (url.returnFormat eq "wddx")>
  •  
  • <!--- Convert the result to XML. --->
  • <cfwddx
  • action="cfml2wddx"
  • input="#local.result#"
  • output="local.responseData"
  • />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/xml" />
  •  
  • <cfelse>
  •  
  • <!--- Convert the result to string. --->
  • <cfset local.responseData = local.result />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/plain" />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Now that we have our response data and mime type
  • variables defined, we can stream the response back
  • to the client.
  • --->
  •  
  • <!--- Convert the response to binary. --->
  • <cfset local.binaryResponse = toBinary(
  • toBase64( local.responseData )
  • ) />
  •  
  • <!---
  • Set the content length (to help the client know how
  • much data is coming back).
  • --->
  • <cfheader
  • name="content-length"
  • value="#arrayLen( local.binaryResponse )#"
  • />
  •  
  • <!--- Stream the content. --->
  • <cfcontent
  • type="#local.responseMimeType#"
  • variable="#local.binaryResponse#"
  • />
  •  
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  • </cfcomponent>

Notice that I've added an OnApplicationStart() event handler so that I can create our remote object cache:

application.apiCache

This struct will take component instances keyed using their path (as defined by the OnCFCRequest() method argument). Then, once inside the OnCFCRequest() event handler, I check to see if the API object already exists in the cache. If it does not, then I instantiate it and cache it. Once that is done, every subsequent remote CFC request to that object can simply pull the existing instance out of the cache.

OnMissingMethod() Functionality

When I first started playing around with this, I thought, Hey, because we are manually instantiating the target CFCs in our remote calls using OnCFCRequest(), we can now take full advantage of ColdFusion's OnMissingMethod() functionality introduced in ColdFusion 8. After all, because we are dealing with a true instance of the target component from within the ColdFusion application framework, requested a method that doesn't exist, should fire the OnMissingMethod() handler (if it exist).

So, I started to wire this all together. First, I went into my Service.cfc and adding an OnMissingMethod() handler that would simply return an array of the result of calling GetGirl() - I was planning to call "GetGirls" in the demo:

Service.cfc

  • <cfcomponent
  • output="false"
  • hint="I am a service with some remote access.">
  •  
  • <cffunction name="Init">
  • <cfreturn this />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="getGirl"
  • access="remote"
  • returntype="struct"
  • returnformat="json"
  • output="false"
  • hint="I return a girl object.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="name"
  • type="string"
  • required="true"
  • hint="I am the name of the girl being returned."
  • />
  •  
  • <!--- Define the girl object. --->
  • <cfset local.girl = {
  • name = arguments.name
  • } />
  •  
  • <!--- Return girl object. --->
  • <cfreturn local.girl />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onMissingMethod"
  • access="public"
  • returntype="any"
  • returnformat="json"
  • output="false"
  • hint="I handle missing method calls.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="missingMethodName"
  • type="string"
  • required="true"
  • hint="I am the requested method."
  • />
  •  
  • <cfargument
  • name="missingMethodArguments"
  • type="struct"
  • required="true"
  • hint="I am the method arguments."
  • />
  •  
  • <!---
  • Create an array containing a girl with the given
  • name, which will get from getGirl().
  • --->
  • <cfset local.girls = [
  • this.getGirl(
  • arguments.missingMethodArguments.name
  • )
  • ] />
  •  
  • <!--- Return girls array. --->
  • <cfreturn local.girls />
  • </cffunction>
  •  
  • </cfcomponent>

Notice that it merely wraps the GetGirl() result in an implicit array [].

Then, I went into my OnCFCRequest() method and updated it to work with OnMissingMethod() events. Because we are getting the ReturnFormat from the target method, we had to add that to our URL.ReturnFormat parameterization logic:

OnCFCRequest() With OnMissingMethod Logic

  • <cffunction
  • name="onCFCRequest"
  • access="public"
  • returntype="void"
  • output="true"
  • hint="I process the user's CFC request.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="component"
  • type="string"
  • required="true"
  • hint="I am the component requested by the user."
  • />
  •  
  • <cfargument
  • name="methodName"
  • type="string"
  • required="true"
  • hint="I am the method requested by the user."
  • />
  •  
  • <cfargument
  • name="methodArguments"
  • type="struct"
  • required="true"
  • hint="I am the argument collection sent by the user."
  • />
  •  
  • <!---
  • Check to see if the target CFC exists in our cache.
  • If it doesn't then, create it and cached it.
  • --->
  • <cfif !structKeyExists( application.apiCache, arguments.component )>
  •  
  • <!---
  • Create the CFC and cache it via its path in the
  • application cache. This way, it will exist for
  • the life of the application.
  • --->
  • <cfset application.apiCache[ arguments.component ] =
  • createObject(
  • "component",
  • arguments.component
  • ).init()
  • />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • ASSERT: At this point, we know that the target
  • component has been created and cached in the
  • application.
  • --->
  •  
  •  
  • <!--- Get the target component out of the cache. --->
  • <cfset local.cfc = application.apiCache[ arguments.component ] />
  •  
  • <!---
  • Execute the remote method call and store the response
  • (note that if the response is void, it will destroy
  • the return variable).
  • --->
  • <cfinvoke
  • returnvariable="local.result"
  • component="#local.cfc#"
  • method="#arguments.methodName#"
  • argumentcollection="#arguments.methodArguments#"
  • />
  •  
  • <!---
  • Create a default response data variable and mime-type.
  • While all the values returned will be string, the
  • string might represent different data structures.
  • --->
  • <cfset local.responseData = "" />
  • <cfset local.responseMimeType = "text/plain" />
  •  
  • <!---
  • Check to see if the method call above resulted in any
  • return value. If it didn't, then we can just use the
  • default response value and mime type.
  • --->
  • <cfif structKeyExists( local, "result" )>
  •  
  • <!---
  • Check to see what kind of return format we need to
  • use in our transformation. Keep in mind that the
  • URL-based return format takes precedence. As such,
  • we're actually going to PARAM the URL-based format
  • with the default in the function. This will make
  • our logic much easier to follow.
  •  
  • Because we might be hitting onMissingMethod(), we
  • need to check to see if the target method that
  • exists. If not, then, we need to
  •  
  • NOTE: This expects the returnFormat to be defined
  • on your CFC - a "best practice" with remote
  • method definitions.
  • --->
  • <cfif structKeyExists( local.cfc, arguments.methodName )>
  •  
  • <!--- Get target method return format. --->
  • <cfparam
  • name="url.returnFormat"
  • type="string"
  • default="#getMetaData( local.cfc[ arguments.methodName ] ).returnFormat#"
  • />
  •  
  • <cfelseif structKeyExists( local.cfc, "onMissingMethod" )>
  •  
  • <!--- Get on missing method return format. --->
  • <cfparam
  • name="url.returnFormat"
  • type="string"
  • default="#getMetaData( local.cfc[ 'onMissingMethod' ] ).returnFormat#"
  • />
  •  
  • </cfif>
  •  
  • <!---
  • Now that we know the URL scope will have the
  • correct format, we can check that exclusively.
  • --->
  • <cfif (url.returnFormat eq "json")>
  •  
  • <!--- Convert the result to json. --->
  • <cfset local.responseData = serializeJSON( local.result ) />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/x-json" />
  •  
  • <cfelseif (url.returnFormat eq "wddx")>
  •  
  • <!--- Convert the result to XML. --->
  • <cfwddx
  • action="cfml2wddx"
  • input="#local.result#"
  • output="local.responseData"
  • />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/xml" />
  •  
  • <cfelse>
  •  
  • <!--- Convert the result to string. --->
  • <cfset local.responseData = local.result />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/plain" />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Now that we have our response data and mime type
  • variables defined, we can stream the response back
  • to the client.
  • --->
  •  
  • <!--- Convert the response to binary. --->
  • <cfset local.binaryResponse = toBinary(
  • toBase64( local.responseData )
  • ) />
  •  
  • <!---
  • Set the content length (to help the client know how
  • much data is coming back).
  • --->
  • <cfheader
  • name="content-length"
  • value="#arrayLen( local.binaryResponse )#"
  • />
  •  
  • <!--- Stream the content. --->
  • <cfcontent
  • type="#local.responseMimeType#"
  • variable="#local.binaryResponse#"
  • />
  •  
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>

Once I had this wired up, I got super excited and called the following URL:

Service.cfc?method=getGirls&name=Tricia

Because "getGirls" does not exist, the OnCFCRequest() event handler should have routed the request through the OnMissingMethod() method on the Service.cfc (implicitly) and returned and array. But, instead, what I got was this ColdFusion error:

The method getGirls was not found in component Service.cfc.

WHAT?!?!?! When I saw this, I almost blew my top (I had spend 15 minutes wiring this all up and debugging the parts). It doesn't work? Are you kidding me?

It appears as if ColdFusion is actually checking the target component meta data to see if the API request can be made before it even passes it off to the OnCFCRequest() event handler. This is clearly and blatantly a bug in the Application.cfc framework as it completely defeats the intent of have request-handlers in the Application.cfc! If the target file exists (Service.cfc), the surrounding data (ex. method name parameter) should be completely irrelevant - it should be up to the request handler to deal with that scenario.

Overall, I think the OnCFCRequest() event handler is interesting, but I don't know how useful it actually is. Yes, we can now cache our target API objects, but is the minor instantiation cost of a thin API layer worth the overhead of all the manual instantiation and caching? Maybe it is, maybe it isn't. If they can fix the serious OnMissingMethod() bug, then there might be some more use. For now, however, it seems that the best request-handling update is the fact that the OnRequest() event method will not be triggered for CFC-based requests.




Reader Comments

It would also be nice if CF exposed the WSDL stuff more. When I hit foo.cfc?wsdl, CF automatically converts the result until WSDL. When a WS hit to foo.cfc, method=goo is run, CF automatically converts the response to WSDL.

So the question is - why don't we have access to this? Why can't we do returnFormat=WSDL? Or SerializeWSDL(cf data)?

If we had those, then we could use them in onCFCRequest.

Reply to this Comment

@Raymond,

Word up. Of course, by the time we start to have to think about WSDL... and then AMF... we probably will have bitten off more than we can chew. Already, as you can see, there's a lot of overhead just to replicate what inherently works with ColdFusion auto-wiring.

Of course, if you just solve the problem once, it's solved, so that's not a great argument :)

Reply to this Comment

Assuming, and I think it is fair to assume, that they fix the missing method thing, then this would be very useful just for Ajax itself.

Reply to this Comment

And by ajax itself, I also mean REST in general, ie, if it isn't useful for FlashRemoting, Web Services, it could still be darn useful.

Reply to this Comment

@Raymond,

True, and considering that RESTful stuff is the only thing I have experience with... life would be sweet :D

Reply to this Comment

I agree that this could be useful, but I'm not sure it is worth breaking web services and flex remoting. Has anyone tested this with flex remoting?

Also I have one request. In the future could you please post the bug number so we can easily vote on your issues? I found this bug, #78816, but sometimes it is difficult to find things in the bug tracker. Thanks!

Reply to this Comment

I agree Ben, this should be a bug, or at least for me its unexpected behavior. When they first announced this new feature of being able to remotely call methods directly on a CFC via a URL I tried it out and was disappointed onMissingMethod wasn't supported. Hopefully your insistence can push for this in the final release of CF9. ;)

Reply to this Comment

@Nathan,

Ahh, I didn't realize bugs got numbers... or there was any voting :) I'm new to that. Will do that in the future.

@Javier,

Word up. Finger crossed for final release.

Reply to this Comment

@Ben - You are finding some really important bugs and I want to make sure Adobe knows about them! I'm sure the CF team reads your blog, but just in case ;)

Reply to this Comment

Hi Ben
I agree this is a bit cock-eyed.

One would also think that if CF is going to the effort of checking this sort of thing, it'd also be checking if the method being called is actually specified as remotely accessible in the first place. It *should* block remote calls to non-remote methods.

Using your approach to calling the CFC / method (which I think is the natural way of doing it), one can inadvertently open up all public methods to being remotely called, unless you yourself perform this check.

I don't think the component should be exposed to the onCfcRequest() handler as dotted-notation, it should be exposed as the URL that was requested in the first place, that way encouraging the internal call to it to be done as a remote call still. I think Adobe is second-guess here in an attempt at being helpful, and causing problems rather than solving them.

If a method is expecting to be called remotely, then that's how it should be called. For one thing, if the method was actually called remotely, you'd not need to horse around with your JSON/WDDX conversions, because the method would do it for you (via the returnFormat setting).

Either we're missing something, or Adobe have not thought this through.

--
Adam

Reply to this Comment

@Adam,

From the emails I have received, Adobe has fixed the method-existence bug for the final release (Adobe has actually fixed all the bugs I've filed so far - rock on Adobe!!). That will make this more useful.

Of course, remember, you only use the OnCFCRequest() method if you *want* to. If not, leave it out and all the CFCs will run as normal.

Reply to this Comment

Can something like onCFCRequest() be reproduced (faked) in CF8
I would like to intercept CFC calls from a single point
I want to interact with the objects in the MethodArguments so I can log/audit application activity in a very sprawling legacy application
This makes me really wish we were already using CF9 but I do not have that luxury at this time

Reply to this Comment

I have been trying to get oncfcrequest working with Flex Remote Object.

I found that when you use an mx:Model your arguments get placed in an array.

arguments.args[1] will be what you pass on to the cfc and not arguments.args.

<mx:RemoteObject id="ro_cf" destination="ColdFusion" source="auth" showBusyCursor="true" >
<mx:method name="login" result="loginResult()" fault="Alert.show(event.fault.message)">
</mx:RemoteObject>

<mx:Model id="credsModel" >
<creds>
<USERNAME>{username.text}</USERNAME>
<PASSWORD>{password.text}</PASSWORD>
</creds>
</mx:Model>

Called with
ro_cf.login(credsModel) ;

Reply to this Comment

@Mike,

That's interesting. I know very little about FLEX and the way it communicates with CFCs. Is this the thing that needs an endpoint or something defined in the configuration? I have always been curious about this because I think FLEX communicates with CFCs in a different way than anything else.

That's odd that it puts the arguments in to an array and stuffs it into the first argument. It sounds like it's not getting wired properly.

Reply to this Comment

@Ben - the flex call defines a destination="ColdFusion" This gets resolved in the services-config file into a channel and then to a servlet mapping called flex2gateway. The mapping in the web.xml then goes to the messagebrokerservlet. So the messagebrokerservlet is the code that then makes the call into the cfc engine of CF.

It's at that point the data gets transferred into CF datatypes and we find ourselves in the application.cfc.

I know I used mx:model for large forms so it will be an issue for anyone using it to send in Flex forms etc.

I recommend using charlesproxy.com to debug any issue like this, it makes it pretty clear what is being passed and returned.

Reply to this Comment

@Mike,

The whole flex2gateway is a mystery to me. I think in the latter half of this year, after my presentations, I'm really going to put some effort into learning some Flex. I feel like it's the one really huge blind spot in my understanding of the ColdFusion application framework.

Reply to this Comment

@Ben

Awesome post! I'm so behind on the times because our client base takes so long to update. As of yesterday I'm finally able to code to CF9. GiddyUp!

I made a few tweaks to the code in case you were interested. Long version can be found here: http://stackoverflow.com/a/22842960/3112803

Short version...
(1) The first addition was to add the code for the "access" bug Adam Cameron found. http://cfmlblog.adamcameron.me/2013/04/its-easy-to-create-security-hole-in.html

(2) The second addition was to do this right after the invoke: <cfset local.result = ToString(local.result)>. You say "...all the values returned will be string..." but that doesn't seem to be the case. We have some remote functions that only return a number. Without the ToString() the code further down that converts the response to binary fails.

(3) Down in the section where the mimeTypes are being set I altered the IF statement for json. In every remote function we've written we make a ColdFusion structure then return it like so: <cfreturn SerializeJSON(z.response) />. This seems a lot easier than piecing together a json string manually, THEN serializing it in the onCFCRequest. So in the onCFCRequest for the json mimeType I just treat it as a string because it's already serialized; so no need to serialize it second time.

(4) Also in the mimeType section I added an IF statement for xml. We have many remote functions which spit out xml for grids, not wddx. And since there is not a returnFormat of xml I added a check for the returnTYPE of xml right above the wddx check.

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.