Learning ColdFusion 9: Using OnCFCRequest() To Return JSONP (JSON With Padding) API Responses
A few days ago, I blogged about the new OnCFCRequest() event handler in ColdFusion 9's Application.cfc framework. This event handler controls requests made to ColdFusion components as opposed to the OnRequest() event handler which, in ColdFusion 9, only handles requests made to ColdFusion templates (CFM file). Over the weekend, I was thinking more about this new method and I thought of a great use case for it: returning JSONP requests. JSONP stands for "JSON With Padding" and is a technique in which JSON data is wrapped in a Javascript method call before it is returned to the client. You can read more about this technique elsewhere, but it is essentially a way to perform GET-based cross-site AJAX requests.
JSON and JSONP return different types of data. JSON returns a mime type of "text/x-json" whereas a JSONP request returns a mime type of "text/javascript". So, for example, if a regular JSON response looked like this:
{"name":"Tricia"}
... which is plain text, a JSONP request with the callback "doSomething", would look like this:
doSomething( {"name":"Tricia"} );
Notice here that rather than returning a JSON response, we are actually returning a line of executable Javascript code (hence the mime type text/javascript).
ColdFusion 9's new OnCFCRequest() event handler is the perfect way to serve up JSONP responses because it proxies the actual CFC request. Therefore, it can check to see if the given request has a "callback" parameter; and, if it does, it can transform its return JSON data into a JSONP Javascript execution. To demonstrate this, I am going to take the Application.cfc from my previous OnCFCRequest() demo, pair it down, and then add a bit of response logic:
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.
--->
<!--- Get target method return format. --->
<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.
NOTE: When checking for JSON responses, check to
see if there is a callback. If there is, then we
are executing a JSONP (JSON with Padding) response
rather than an actual JSON response.
--->
<cfif (
(url.returnFormat eq "json") &&
!structKeyExists( url, "callback" )
)>
<!--- 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 "json") &&
structKeyExists( url, "callback" )
)>
<!---
This is a JSONP (JSON with Padding) resposne.
We need to wrap the JSON response in the
provided callback execution:
callback( JSON_REPONSE );
--->
<cfset local.responseData = (
"#url.callback#(" &
serializeJSON( local.result ) &
");"
) />
<!--- Set the appropriate mime type. --->
<cfset local.responseMimeType = "text/javascript" />
<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>
Most of this is from my previous post; but, if you look at my CFIF/CFELSEIF statements, you will notice this new logic:
<!---
Now that we know the URL scope will have the
correct format, we can check that exclusively.
NOTE: When checking for JSON responses, check to
see if there is a callback. If there is, then we
are executing a JSONP (JSON with Padding) response
rather than an actual JSON response.
--->
<cfif (
(url.returnFormat eq "json") &&
!structKeyExists( url, "callback" )
)>
<!--- 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 "json") &&
structKeyExists( url, "callback" )
)>
<!---
This is a JSONP (JSON with Padding) resposne.
We need to wrap the JSON response in the
provided callback execution:
callback( JSON_REPONSE );
--->
<cfset local.responseData = (
"#url.callback#(" &
serializeJSON( local.result ) &
");"
) />
<!--- Set the appropriate mime type. --->
<cfset local.responseMimeType = "text/javascript" />
<!--- .... Other response cases here .... --->
</cfif>
Notice that if we have a JSON request, the OnCFCRequest() logic checks to see if there is a callback parameter in the URL scope. If there is a callback parameter, rather than returning JSON data, it wraps the JSON data in a Javascript method execution using the name of the given callback and returns it as "text/javascript" data.
To test this, I set up a small page that makes two AJAX requests to the remote Service.cfc (previously discussed here). This page makes one requesting for a standard JSON response and one requesting for a JSONP response:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>ColdFusion 9 OnCFCRequest() JSONP Demo</title>
<script type="text/javascript" src="jquery-1.3.2.min.js"></script>
<script type="text/javascript">
$(function(){
// Make a regular JSON call.
$.ajax({
type: "get",
url: "./Service.cfc",
data: {
method: "getGirl",
name: "Tricia",
returnFormat: "json"
},
dataType: "json",
success: function( response ){
console.log( "JSON Response:" );
console.log( response );
}
});
// Make a regular JSON with PADDING call.
$.ajax({
type: "get",
url: "./Service.cfc",
data: {
method: "getGirl",
name: "Tricia",
returnFormat: "json"
},
dataType: "jsonp",
success: function( response ){
console.log( "JSONP Response:" );
console.log( response );
}
});
});
</script>
</head>
<body>
<!--- Nothing to show here. --->
</body>
</html>
Notice that the second jQuery-powered AJAX response defines its dataType as "jsonp." In doing so, jQuery knows to define a temporary callback method and pass it along with the AJAX request. When we run the above code, we get the following responses:
{"NAME":"Tricia"}
... and:
jsonp1248093721267({"NAME":"Tricia"});
As you can see, the second request returned an actual line of executable Javascript code using the callback method that jQuery passed in along with the API request.
In ColdFusion 8, if you wanted to return a JSONP response in your CFC-based API, you would have to check for it from within the remote method call and make sure that your ReturnFormat was "plain" and not "json". I think we can all agree that this is a bit of hack and requires the remote method to take on more responsibility that it was designed to. Now, with ColdFusion 9's OnCFCRequest(), we can make JSONP part of the API request/response framework, leaving the individual API objects with the sole responsibility of returning their natural data. The more I think about it, the more I am liking the OnCFCRequest() event handler.
Want to use code from this post? Check out the license.
Reader Comments
Ben,
That is some pretty nice stuff right there! Always a bit trick trying to figure out the design pattern for ajax vs. non-ajax requests.
I especially like that the cfc doesn't have to know anything about what the externals of the app. Just return the data, and let something else do all the work.
Pretty awesome.
@Brandon,
As far as the AJAX vs. non-AJAX requests, I think JSONP might make a great argument for CFM-based AJAX calls rather than CFC based AJAX calls. Perhaps I'll touch upon that in a blog post.
ColdFusion 9 isn't even out yet and you are already demoing framework enhancements. Nice job!
@Drew,
Thanks Drew, I'm pretty excited about all this stuff.
I always seem to find your posts whenever I'm researching an issue. I just updated mime types on my server and I saw that you were returning json data using the mime type "text/x-json". This caused me to research this further and I found that this article states that "application/json" is the official adopted mime:
http://stackoverflow.com/questions/477816/the-right-json-content-type
@James,
Great link! Thanks. Yeah, I never really know what the best thing is. I've used text/json, application/json, and application/x-json. Good to have a definitive answer. That said, I should probably double-check the ColdFusion framework and API presenations I am about to give that make use of JSON data :D
Once again, saved by Ben! Perfect solution for my new ventures into mobile development. Thanks!