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