Using OnMissingMethod() With Remote Access ColdFusion Components
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