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!