Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Luis Majano and Jürg Anderegg
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Luis Majano Jürg Anderegg

Using OnMissingMethod() With Remote Access ColdFusion Components

By
Published in

After my breakthrough discovery last week that the response of a remote access ColdFusion component method call was in no way tied to the method invocation itself, I wanted to see if this technique could be updated to allow the OnMissingMethod() function to be used with remote access method calls. I don't necessarily think that there is really any need to use the OnMissingMethod() function in the context of an API, but nonetheless, I thought it would be fun to try.

As you can see from the video, the technique that I outlined last week can be used to wire up the OnMissingMethod() function to work with remote access API method calls. Doing so was actually easy - it was just a matter of fleshing out the OnError() event handler a bit more. But, now that I'm manually streaming back an API response in two different scenarios (uncaught errors and missing methods), I refactored the RemoteProxy.cfc base component to be more modular. This way, I can leverage the manual streaming and the error response in multiple places.

RemoteProxy.cfc

<cfcomponent
	output="false"
	hint="I provide the core remote proxy API functionality.">


	<cffunction
		name="NewAPIResponse"
		access="public"
		returntype="struct"
		output="false"
		hint="I provide a new default API response object.">

		<!--- Define the local scope. --->
		<cfset var LOCAL = {} />

		<!--- Create a new API response object. --->
		<cfset LOCAL.Response = {
			Success = true,
			Errors = [],
			Data = ""
			} />

		<!--- Return new response object. --->
		<cfreturn LOCAL.Response />
	</cffunction>


	<cffunction
		name="ManuallyReturnReponse"
		access="public"
		returntype="void"
		output="false"
		hint="I stream the given response to the client manually using CFHeader and CFContent.">

		<!--- Define arguments. --->
		<cfargument
			name="Response"
			type="struct"
			required="true"
			hint="I am the API response being returned."
			/>

		<cfargument
			name="ReturnFormat"
			type="string"
			required="false"
			default="JSON"
			hint="I am the format required for the API response."
			/>

		<!--- Define the local scope. --->
		<cfset var LOCAL = {} />

		<!---
			Create the response string. Check to see if we want
			this as WDDX or JSON.
		--->
		<cfswitch expression="#ARGUMENTS.ReturnFormat#">
			<cfcase value="wddx">

				<!--- Convert to WDDX. --->
				<cfwddx
					action="cfml2wddx"
					input="#ARGUMENTS.Response#"
					output="LOCAL.ResponseString"
					/>

			</cfcase>
			<cfdefaultcase>

				<!---
					By default, we are going to return the API
					response in JSON string (since that is what
					our appliation does by default).
				--->
				<cfset LOCAL.ResponseString = SerializeJSON(
					ARGUMENTS.Response
					) />

			</cfdefaultcase>
		</cfswitch>


		<!---
			Now that we have our API response string, we need
			to update the headers and return the response.
		--->

		<!---
			Set header response code to be 200 - remember, the
			whole point of the unified API response is that it
			never "fails" unless there is truly a request
			exception.
		--->
		<cfheader
			statuscode="200"
			statustext="OK"
			/>

		<!--- Steam binary contact back. --->
		<cfcontent
			type="text/plain"
			variable="#ToBinary( ToBase64( LOCAL.ResponseString ) )#"
			/>

		<!---
			At this point, the request has been completely
			committed and cannot be altered.
		--->

		<!--- Return out. --->
		<cfreturn />
	</cffunction>


	<cffunction
		name="OnMissingMethod"
		access="public"
		returntype="struct"
		output="false"
		hint="I handle non-explicit API messaging.">

		<!--- Define arguments. --->
		<cfargument
			name="MethodName"
			type="string"
			required="true"
			hint="I am the name of the method."
			/>

		<cfargument
			name="MethodArguments"
			type="struct"
			required="true"
			hint="I am the collection of arguments."
			/>

		<!---
			Use the error response to create and return an API
			response with the given API error.
		--->
		<cfreturn THIS.NewErrorResponse( "The method that you requested, #ARGUMENTS.MethodName#, is not supported in the current version of this API." ) />
	</cffunction>


	<cffunction
		name="NewErrorResponse"
		access="public"
		returntype="struct"
		output="false"
		hint="I create an return a new error response with the given error.">

		<!--- Define arguments. --->
		<cfargument
			name="Error"
			type="string"
			required="true"
			hint="I am the error message."
			/>

		<!--- Define the local scope. --->
		<cfset var LOCAL = {} />

		<!--- Create a new API response object. --->
		<cfset LOCAL.Response = THIS.NewAPIResponse() />

		<!--- Flag it as not successful. --->
		<cfset LOCAL.Response.Success = false />

		<!--- Set the error message. --->
		<cfset LOCAL.Response.Errors[ 1 ] = {
			Property = "",
			Error = ARGUMENTS.Error
			} />

		<!--- Return new response. --->
		<cfreturn LOCAL.Response />
	</cffunction>

</cfcomponent>

The big difference here from my previous demonstration is that none of the error methods in the RemoteProxy.cfc explicitly call the ManuallyReturnReponse() method to manually stream the API response back to the client. I decided to move that level of control up to the Application.cfc. Sure, the RemoteProxy.cfc sill has the "utility" method for manually returning the response, but in order to better reuse the code, I wanted to move the actual call out of the remote objects. Plus, philosophically speaking, the remote objects shouldn't really know when to call ManuallyReturnReponse() as this method is actually just a form of error handling to be leveraged by the application architecture.

To get this all wired together nicely, I expanded the OnError() event handler in the Application.cfc to look for both uncaught errors and missing method errors. After some trial an error, I found that I could determine the missing method errors by the existence of the "Func" key in the Exception object. Of course, I don't ever need to use this key as the requested Method is always defined in the URL/FORM scope of the API request.

Application.cfc

<cfcomponent
	output="false"
	hint="I provide application settings and event handlers.">


	<cffunction
		name="OnError"
		access="public"
		returntype="void"
		output="true"
		hint="I handle uncaught application errors.">

		<!--- Define the arguments. --->
		<cfargument
			name="Exception"
			type="any"
			required="true"
			hint="I am the uncaught exception."
			/>

		<!--- Define the local scope. --->
		<cfset var LOCAL = {} />

		<!---
			Normally, we would want to return a error header, but
			if the request was an API call (remote call), we never
			want to return an error. Rather, we want to return an
			"unsuccessful" API request.
		--->
		<cfif REFindNoCase( "\.cfc$", CGI.script_name )>

			<!---
				Create an instance of the CFC in question.

				NOTE: This line of code leverages the undocumented
				ability for ColdFusion components to be created
				both dot-delimited AND slash-delimited file paths.
			--->
			<cfset LOCAL.API = CreateObject(
				"component",
				REReplaceNoCase(
					CGI.script_name,
					"\.cfc$",
					"",
					"one"
					)
				) />


			<!--- Set default return format. --->
			<cfset LOCAL.ReturnFormat = "JSON" />

			<!--- Check to see if return format was overriden. --->
			<cfif StructKeyExists( URL, "ReturnFormat" )>

				<!--- Overriding default format. --->
				<cfset LOCAL.ReturnFormat = URL.ReturnFormat />

			</cfif>


			<!---
				Check to see what kind of error we had. If the
				"func" key exists, then it was a missing method
				error. If not, then it was simply an unhandled
				error.
			--->
			<cfif StructKeyExists( ARGUMENTS.Exception, "Func" )>

				<!--- Missing method error. --->

				<!--- Build up arguments. --->
				<cfset LOCAL.Arguments = Duplicate( URL ) />
				<cfset StructAppend( LOCAL.Arguments, FORM ) />

				<!---
					Call onMissingMethod and stream back response
					to client manually.
				--->
				<cfset LOCAL.API.ManuallyReturnReponse(
					LOCAL.API.OnMissingMethod(
						LOCAL.Arguments.Method,
						LOCAL.Arguments
						),
					LOCAL.ReturnFormat
					) />

			<cfelse>

				<!--- Uncaught exception. --->

				<!---
					Create a new error and return it to the client
					using explicit stream.
				--->
				<cfset LOCAL.API.ManuallyReturnReponse(
					LOCAL.API.NewErrorResponse(
						ARGUMENTS.Exception.Message
						),
					LOCAL.ReturnFormat
					) />

			</cfif>

		</cfif>

		<!--- Return out. --->
		<cfreturn />
	</cffunction>

</cfcomponent>

As you can see, the OnError() event handler checks to see which CFC was accessed. It then creates a local instance of that CFC and explicitly calls the ManuallyReturnReponse() on the API object. Depending on what type of error occurred (missing method or uncaught exception), the OnError() event handler gets different API responses from the given CFC and hands them off to the ManuallyReturnReponse() method for manual streaming.

So there you go - with a small amount of error handling, you can catch uncaught exceptions and handling missing methods while still maintaining a uniform API response. I love the error handling in my previous blog post; the use of OnMissingMethod() was more for fun - I'm not sure this would ever have a great value in the context of an API.

NOTE: None of the other file in this demonstration were changed. If you want to see the client-side scripting, please view my previous post.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel