Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Learning ColdFusion 9: Using OnCFCRequest() To Return JSONP (JSON With Padding) API Responses

By Ben Nadel on
Tags: ColdFusion

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.



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.

@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