REST Approaches

Hi folks,

As you’ve probably seen from some of my emails lately I’m working on a REST based system using CB. For the most part it has been going very well and I’ve been able to leverage the SES Interceptor and custom routes to route requests to the proper handlers as well as enforce proper VERB security on my handler methods.

My Rest endpoints are structured like this:

…/user/{userid} - Get a user
…/user/{userid}/contacts/ - Get a list of contacts by user
…/user/{userid}/contacts/{contactid} - Get contact detail for user

I’ve been able to accomplish this by removing the default route in routes.cfm (/:handler/:action) and have created routes like so:

// ********** Contact endpoints *********** //

//Get Contact
addRoute(pattern=“user/:userid/contacts/:contactid”,
handler=“Contact”,
action={
GET = “getContact”
});

//Get User Contacts

addRoute(pattern=“user/:userid/contacts”,
handler=“Contact”,
action={
GET = “getUserContacts”
});

// ********** User endpoints *********** //

//Get User
addRoute(pattern=“user/:userid”,
handler=“User”,
action={
GET = “getUser”
});

//Create User
addRoute(pattern=“user/”,
handler=“User”,
action={
POST = “createUser”
});

I am wondering if you think this is a valid approach given my endpoints? I thought about using each handler’s onMissingMethod() function so have loose routing however this approach doesn’t allow me to enforce the property routing based on the REST VERB.

None of the handlers render any data within a view, rather they send back the results via event.renderData() as either JSON or XML.

I have been running into an issue where if I have a service call in a handler and the service call has an error I don’t get an exception thrown with the service call error. Instead the exception gracefully fails and the handler tries to render a view corresponding to the handler method that was called. i.e can’t find view: views/getuser.cfm

I’m not sure what is going on here and it’s making debugging service calls extremely difficult.

I’m also looking for an approach to enforce HTTPSecurity prior to any handler being hit. would this be done within the security interceptor? the preHandler() function of the handler() or should this be done within Main.onRequestStart()?

Thanks for your time. Appreciate any thoughts on this approach.

Regards,

Nolan

Nolan,

For errors I recommend the following strategy.

Create a BaseRestHandler component that your RESTFul handlers inherit from. In the Base class, create the method: onError() which you can see from the generated samples. The onError() method is a new convention in 3.0 that intercepts whenever ANY runtime exception occurrs within a handler. Therefore it will intercept the exception and you can return a RESTful error to the users.

However, I would also recommend having the highest level using an onException interceptor or custom exception handler that can be your last line of defense. This will give you consistency to your errors that are RESTful.

I also recommend that you do the renderData at the base rest handler level, so you decouple the rendering from the restuful handlres themselves and let the base handler do the conversions and setup via pre and postHandler().

The simpleblog example on the samples download has a similar approach.

Luis F. Majano
President
Ortus Solutions, Corp

ColdBox Platform: http://www.coldbox.org
Linked In: http://www.linkedin.com/pub/3/731/483
Blog: http://www.luismajano.com
IECFUG Manager: http://www.iecfug.com

Hi guys,

Luis, thanks so much for your email. I was looking through GITHub and came across the iphone REST demo you built for this years CFUnited. Very cool! I wish I had seen these earlier :wink:
Nonetheless, reviewing the code has clarified what you describe below. One question though…

Within my REST app there are situations where a successful request is not a 200 OK. i.e. status code 204 indicates that the request was Accepted and is being processed. I have a handler function created which sets the HTTP Header to 204 and calls the postHandler event. The function is much like the onError() function of the BaseRestHandler.cfc in that it overrides the HTTP StatusCode.

function createReferral(event){
var rc = event.getCollection();

var rc.referral = shareService.sendReferral(myobj);

//returns 204 if successful
rc.results.error = false;
rc.results.messages = “The referral was accepted”;

// Headers
setHeader(204,rc.results.messages);
postHandler(event);

}

Despite setting the header to 204, the request still gets returned as a 200, along with the above results.error and results.messages. Am I overriding the HTTP Status Code incorrectly? Should this be accomplished another way?

Thanks so much.

Nolan

Sorry,

just so I’m accurate… Status code 204 or No Content. 202 is Accepted :wink: Nonetheless, calling setHeader(204,“message”) at the handler level doesn’t seem to have an effect.

Nolan

Hi Luis,

After locking down specific handler methods to certain verbs I ran a test to try doing a GET on a method that was only allows a PUT. I received the below exception, which is what I expected, however the HTTPStatus code came back as 200 OK. As an aside, I’m using Firefox Poster extension to run my tests.

{“error”:true,“messages”:“The action:updateGroup failed: The requested event: Group.updateGroup cannot be executed using the incoming HTTP request method ‘GET’. 403 Invalid HTTP Method Exception”}

Is 200 to supposed to be the returned status code when invalid HTTP Method Exceptions occur?

Thank you.

Nolan

Nolan, how exactly are you returning the data from your handler? If you are using event.renderData(), you can pass in the statusCode and statusText you want.

~Brad

Hi Brad,

Each of my handlers extend a BaseHandler. The BaseHandler CFC contains a preHandler, postHandler and methods to prepare the results to be returned and set the HTTP Header. Here is the code.

log.debug("Incoming request for route: #event.getCurrentRoute()#"); event.setValue("results", prepareResults()); event.renderData(data=event.getValue("results"),type="json",jsonAsText=true); var results = {};

return results;

var rc = event.getCollection();

// Setup the results
rc.results.error = true;
rc.results.messages = “The action:#faultaction# failed: #exception.detail# #exception.message#”;

// Log it
log.error(rc.results.messages, exception);

// Headers
setHeader(500,rc.results.messages);

postHandler(event);

The post handler handles sending the data back, however in my User handler I’m trying to override the 200 status and for a 404.

function getUser(event){
var rc = event.getCollection();
//query for the user
rc.user = userService.getUser(rc.userid);

//validate user is not empty
if(structIsEmpty(rc.user)){
setHeader(404);
}else{
rc.results = rc.user;
}
}

You’ll see in the onError function about that setHeader() is being called to send the proper status code to the browser when an error occurs. Essentially i’m trying to do the same thing programatically within my handlers.

Thanks.

Nolan

Hi Luis,

Just wanted to follow up on this one to see what your thoughts were. I still haven’t been able to get the status code to come back as 204 when I try to override within my handler.

As Brad indicates I could execute the event.renderData() within my handler and pass in the status code, however the postHandler() event already does the event.renderData(). All i’m looking to do is to override the HTTP status code from within my handler and have the postHandler pick that up. This seems like the most elegant approach and eliminates the need to pepper my handlers with event.renderData(); If this is the approach I need to take though, then so be it :wink:

Thanks all.

Nolan

Can’t you just pass the desired status code to postHandler() in the request collection? Then the regular handler gets to determine the status code, but the postHandler picks it up and uses it.

rc.results.statusCode = 204;

You should be able to do the same thing with statusText

~Brad

Hi Brad,

All that does is puts the statusCode variable into the result object. I could append any info into the response using this approach. This doesn’t actually set the HTTP Header status Code to 204 though. In this case the HTTP header status code would still be 200 OK.

Nolan

I think you need to create a Results object that can have all potential variables. That is what I am doing, but not on the simple app you saw, which just was a plain old structure.

You can model a RESTResults object that can have the status code, text, headers as properties, with a data packet, etc. Then use that when building the service and what the posthandler should look at to construct the RESTful response.

Please note also that this week I committed a new feature called: aroundHandler(), which allows you to completely do an around advice on a targeted event.

aroundHandler(event,targetAction, eventArguments)
This will intercept all event action methods so you can take over and do an around advice. You can control the except only lists exactly like the pre/post handler:
this.aroundHandler_only="";
this.aroundHandler_except="";

You can also do directed around advices via: around{actionName}(event,targetAction, eventArguments).

Why I mention this? Well, you can create an aroundHandler() implicit method for all RESTful services so you can control their execution:

aroundHandler(event, targetAction, eventArguments){
var rc = event.getCollection();
// get Restful Results object
rc.results = getModel(“Results”);

// try the action
try{
// call targeted action
arguments.targetAction(event);
}
catch(Any e){
// handle a restful exception
handleRestfulException(e,rc.results);
}

// prepare the results
event.renderData(data=results.getData(),
format=rc.format,
statusCode=results.getStatusCode(),
statusText=results.getStatusText() );

}

Luis F. Majano
President
Ortus Solutions, Corp

ColdBox Platform: http://www.coldbox.org
Linked In: http://www.linkedin.com/pub/3/731/483
Blog: http://www.luismajano.com
IECFUG Manager: http://www.iecfug.com

Hi Luis,

Thanks for your reply. I’ll checkout the latest build on GIT and give this a shot.

Forgive me, but does the function name aroundHandler() refer to the actual Handler that is being executed? i.e aroundUser(), aroundGroup(), or is the function implemented with the name aroundHandler().

In regards to my app, I would want and around advice to run across the entire app, on all handlers and methods. Is there a way to achieve that across the board, or would there need to be an around advice placed in each handler? Having the granular control to perform an advice on handlers and specific actions is a definite plus! Nice work, and thanks! Do you happen to have any sample apps which use this new feature?

Thank you!

Nolan

Hi Luis,

I’ve upgraded to the latest CB build on GitHub and have been able to implement the aroundHandler. Very cool stuff!

I also created a model to model the REST results like so:

RESTResults.cfc

/**

  • I model the results of a RESTful response
    */
    component accessors=“true”{

property name=“data” type=“any”;
property name=“error” type=“boolean”;
property name=“messages” type=“text”;
property name=“statusCode” type=“numeric” default=“200”;
property name=“statusText” type=“string” default=“OK”;
property name=“jsonAsText” type=“boolean” default=“true”;

RestResults function init(){
return this;
}

}

And here are the functions in my handler. Note, I’ve also removed the code from my preHandler() and postHandler() which used to prepare the rc.results object and execute event.renderData(). This was done because the aroundHandler() now does this.

function aroundHandler(event,targetAction,eventArguments){
var rc = event.getCollection();

// get Restful Results object
var rc.results = getModel(“RESTResults”);

// try the action
try{
// call targeted action
arguments.targetAction(event);
}
catch(Any e){
// handle a restful exception
//handleRestfulException(e,rc.results);
}

// prepare the results
event.renderData(data=rc.results.getData(),
format=rc.format,
statusCode=rc.results.getStatusCode(),
statusText=rc.results.getStatusText(),
jsonAsText=rc.results.getJSONAsText());

}

function getUser(event){
var rc = event.getCollection();
//query for the user
rc.user = userService.getUser(rc.userid);

//validate user is not empty
if(structIsEmpty(rc.user)){
log.info(“Invalid User Request - #rc.userID#|#rc.Authorization# from #cgi.REMOTE_ADDR#, route: #event.getCurrentRoute()#”);
rc.results.setStatusCode(404);
}else{
rc.results.setData(rc.user);
}
}

For the most part everything is working correctly except that an error is thrown when trying to execute data=rc.results.getData() in the aroundHandler. Here is the stack trace:

Application Execution Exception

Error Type: Expression : [N/A]

Error Messages:
Complex object types cannot be converted to simple values.

The expression has requested a variable or an intermediate expression result as a simple value. However, the result cannot be converted to a simple value. Simple values are strings, numbers, boolean values, and date/time values. Queries, arrays, and COM objects are examples of complex values.

The most likely cause of the error is that you tried to use a complex value as a simple one. For example, you tried to use a query variable in a cfif tag.

Tag Context:
ID: CF_CAST
LINE: 276
Template: /Library/WebServer/Documents/coldbox/system/Coldbox.cfc
ID: CF_UDFMETHOD
LINE: 69
Template: /Library/WebServer/Documents/guardly-platform/api/v01/Application.cfc
Framework Snapshot
Current Event: User.getUser
Current Layout: N/A
Current View: N/A
Bug Date: 11/16/2010 05:50:31 PM
Coldfusion ID:

CFID=11300 ;

CFToken=50692952 ;

JSessionID=GUARDLY_API_V01_580E5F528360F62521A9DFA9FA175C30_11300_50692952

Template Path : /Library/WebServer/Documents/guardly-platform/api/v01/index.cfm
Path Info : /user/1.json
Host & Server: 127.0.0.1 Nolan-Dubeaus-MacBook-Pro.local
Query String:
Browser: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12
Stack Trace:
coldfusion.runtime.CfJspPage$ComplexObjectException: Complex object types cannot be converted to simple values.
at coldfusion.runtime.Cast._String(Cast.java:1023)
at cfColdbox2ecfc647030621$funcPROCESSCOLDBOXREQUEST.runFunction(/Library/WebServer/Documents/coldbox/system/Coldbox.cfc:276)
at coldfusion.runtime.UDFMethod.invoke(UDFMethod.java:472)
at coldfusion.runtime.UDFMethod$ReturnTypeFilter.invoke(UDFMethod.java:405)
at coldfusion.runtime.UDFMethod$ArgumentCollectionFilter.invoke(UDFMethod.java:368)
at coldfusion.filter.FunctionAccessFilter.invoke(FunctionAccessFilter.java:55)
at coldfusion.runtime.UDFMethod.runFilterChain(UDFMethod.java:321)
at coldfusion.runtime.UDFMethod.invoke(UDFMethod.java:220)
at coldfusion.runtime.CfJspPage._invokeUDF(CfJspPage.java:2582)
at cfApplication2ecfc1523304759$funcONREQUESTSTART.runFunction(/Library/WebServer/Documents/guardly-platform/api/v01/Application.cfc:69)
at coldfusion.runtime.UDFMethod.invoke(UDFMethod.java:472)
at coldfusion.runtime.UDFMethod$ReturnTypeFilter.invoke(UDFMethod.java:405)
at coldfusion.runtime.UDFMethod$ArgumentCollectionFilter.invoke(UDFMethod.java:368)
at coldfusion.filter.FunctionAccessFilter.invoke(FunctionAccessFilter.java:55)
at coldfusion.runtime.UDFMethod.runFilterChain(UDFMethod.java:321)
at coldfusion.runtime.UDFMethod.invoke(UDFMethod.java:220)
at coldfusion.runtime.TemplateProxy.invoke(TemplateProxy.java:491)
at coldfusion.runtime.TemplateProxy.invoke(TemplateProxy.java:337)
at coldfusion.runtime.AppEventInvoker.invoke(AppEventInvoker.java:88)
at coldfusion.runtime.AppEventInvoker.onRequestStart(AppEventInvoker.java:258)
at coldfusion.filter.ApplicationFilter.invoke(ApplicationFilter.java:349)
at coldfusion.filter.RequestMonitorFilter.invoke(RequestMonitorFilter.java:48)
at coldfusion.filter.MonitoringFilter.invoke(MonitoringFilter.java:40)
at coldfusion.filter.PathFilter.invoke(PathFilter.java:94)
at coldfusion.filter.ExceptionFilter.invoke(ExceptionFilter.java:70)
at coldfusion.filter.BrowserDebugFilter.invoke(BrowserDebugFilter.java:79)
at coldfusion.filter.ClientScopePersistenceFilter.invoke(ClientScopePersistenceFilter.java:28)
at coldfusion.filter.BrowserFilter.invoke(BrowserFilter.java:38)
at coldfusion.filter.NoCacheFilter.invoke(NoCacheFilter.java:46)
at coldfusion.filter.GlobalsFilter.invoke(GlobalsFilter.java:38)
at coldfusion.filter.DatasourceFilter.invoke(DatasourceFilter.java:22)
at coldfusion.filter.CachingFilter.invoke(CachingFilter.java:62)
at coldfusion.CfmServlet.service(CfmServlet.java:200)
at coldfusion.bootstrap.BootstrapServlet.service(BootstrapServlet.java:89)
at jrun.servlet.FilterChain.doFilter(FilterChain.java:86)
at coldfusion.monitor.event.MonitoringServletFilter.doFilter(MonitoringServletFilter.java:42)
at coldfusion.bootstrap.BootstrapFilter.doFilter(BootstrapFilter.java:46)
at jrun.servlet.FilterChain.doFilter(FilterChain.java:94)
at jrun.servlet.FilterChain.service(FilterChain.java:101)
at jrun.servlet.ServletInvoker.invoke(ServletInvoker.java:106)
at jrun.servlet.JRunInvokerChain.invokeNext(JRunInvokerChain.java:42)
at jrun.servlet.JRunRequestDispatcher.invoke(JRunRequestDispatcher.java:286)
at jrun.servlet.ServletEngineService.dispatch(ServletEngineService.java:543)
at jrun.servlet.jrpp.JRunProxyService.invokeRunnable(JRunProxyService.java:203)
at jrunx.scheduler.ThreadPool$ThreadThrottle.invokeRunnable(ThreadPool.java:428)
at jrunx.scheduler.WorkerThread.run(WorkerThread.java:66)

Any ideas as to why this occurs?

Many thanks.

Nolan

So I managed to get this to work, although I’m not sure why I have to reference get the data object via event.getValue(“results”).getData() as opposed to rc.results.getData().

The value of rc.results.getData() is a struct, and it is possible to pass a struct into event.renderData(). The error which occurs if you pass data=rc.results.getData() indicates that the complex type (struct) cannot be converted.

Anyhoo… got it working. If anyone has an idea as to what was up with this issue I’d love to know.

Thanks.

Nolan

function aroundHandler_(event,targetAction,eventArguments){
var rc = event.getCollection();
log.info("///////Around User Advice route: #event.getCurrentRoute()#");
// get Restful Results object
rc.results = getModel(“RESTResults”);

// try the action
try{
// call targeted action
arguments.targetAction(event);
}
catch(Any e){
// handle a restful exception
//handleRestfulException(e,rc.results);
}

// prepare the results
event.renderData(data=event.getValue(“results”).getData(),
type=rc.format,
statusCode=rc.results.getStatusCode(),
statusText=rc.results.getStatusText(),
jsonAsText=rc.results.getJSONasText());

}

function getUser(event){
var rc = event.getCollection();
//query for the user
rc.user = userService.getUser(rc.userid);

//validate user is not empty
if(structIsEmpty(rc.user)){
log.info(“Invalid User Request - #rc.userID#|#rc.Authorization# from #cgi.REMOTE_ADDR#, route: #event.getCurrentRoute()#”);
rc.results.setStatusCode(404);
}else{
//rc.results.setData(rc.user);
rc.results.setData(rc.user);
rc.results.setStatusText(“foobarchu”);
rc.results.setStatusCode(404);
}
}

event.getValue(“results”).getData()

rc = event.getCollection();
rc.results.getData()

Hi Brad,

I applied to get that statusText into the official spec, but I haven’t heard back yet :wink:

Okay, I must be going crazy becuase I just changed my code back to data=rc.results.getData() and it worked. ummmmmmm… this error plagued me yesterday, and now I can’t replicate it. I think one of the emails earlier in the thread may have contained the stack trace. I’ll see if I can replicate, but the fact that it’s working using rc.results.getData() is a good thing and I can keep progressing.

Cheers,

Nolan

Question…

Can anyone confirm if event.renderData() supports passing in null as the data parameter?

I’ve implement an aroundHandler like so but had to hack it a bit to support no data being set by the time event.renderData fires. … In my getUser function I only call setData() if a user record exists otherwise it defaults to null from the implicit getter of the RestResults Model.

RestResults.cfc

/**

  • I model the results of a RESTful response
    */
    component accessors=“true” singleton=“true”{

property name=“data” type=“any” default="";
property name=“error” type=“boolean” default="";
property name=“messages” type=“text” default="";
property name=“statusCode” type=“numeric” default=“200”;
property name=“statusText” type=“string” default=“OK”;
property name=“jsonAsText” type=“boolean” default=“true”;

RestResults function init(){
return this;
}

}

User.cfc (extends BaseRestHandler.cfc)

function aroundHandler(event,targetAction,eventArguments){
var rc = event.getCollection();
// get Restful Results object
var results = getModel(“RESTResults”);

// try the action
try{
// call targeted action
arguments.targetAction(event);
}
catch(Any e){
// handle a restful exception
//handleRestfulException(e,rc.results);
}

if(!isNull(rc.results.getData())){
// prepare the results
event.renderData(data=rc.results.getData(),
type=rc.format,
statusCode=rc.results.getStatusCode(),
statusText=rc.results.getStatusText(),
jsonAsText=rc.results.getJSONasText());

}else{
setHeader(rc.results.getStatusCode(),rc.results.getStatusText());
event.noRender();
}

}

function getUser(event){
var rc = event.getCollection();
//query for the user
rc.user = userService.getUser(rc.userid);

//validate user is not empty
if(structIsEmpty(rc.user)){
log.info(“Invalid User Request - #rc.userID#|#rc.Authorization# from #cgi.REMOTE_ADDR#, route: #event.getCurrentRoute()#”);
rc.results.setStatusText(“User does not exist”);
rc.results.setStatusCode(404);
}else{
//rc.results.setData(rc.user);
rc.results.setData(rc.user);
}
}

SetHeader function in BaseRestHandler.cfc

Would it make sense to allow nulls to be passed to the data= attribute event.renderData()? I think this would be handy as the entire response back to the client is encapsulated in event.renderData, and it allows for proper statusCodes and statusText to be sent back, with null as the content.

Thoughts?

Thanks.

Re-reading this post, I think the cause of this weirdness was that you were var scoping “var results = getModel(“RESTResults”);” so it never left the function.

Please ignore my last comment as I pasted the wrong line. I think the original issue was this line:

var rc.results = getModel(“RESTResults”);