Sunday 20 April 2014

ColdFusion REST services and restSetResponse() revisited

G'day:
Ages ago I wrote an article lamenting the way restSetResponse() has been implemented: "restSetResponse() requires the method to be returntype void. What?". At the time I was looking at how it was instrumental in how "ColdFusion takes something that should be easy and makes it hard" in the context of exception handling. That's slightly edge-case-y, I'll admit it.

But I think I've encountered a standard-operating-procedure situation today which demonstrates the implementation of restSetResponse() is not fit for purpose. Literally: it's not fit for the purpose it has been implemented for.

Today has been a frickin' frustrating day. I sat down to do another backbone.js tutorial ("Backbone.js Beginner Video Tutorial"), having finished "Anatomy of Backbone.js" and "Anatomy of Backbone.js Part 2" from CodeSchool yesterday. I started watching the video @ around 10am, and was inspecting the code on Github at 10:20am. At that point in time I figured I had better knock together the server-side code the tutorial will need: basically some RESTful web services for get-all, get, create, update and delete. Easy. I set out to implement this code using Railo, and had it all operational by 11:30am. At that juncture I started reading up on exactly what I should be returning for the less-obvious situations: the response for a GET is obvious: return the object(s) concerned. But what do I return for a POST? And a DELETE? So I started reading the HTTP spec ("Hypertext Transfer Protocol -- HTTP/1.1 - 9 Method Definitions"), which explained it all clearly.

One interesting thing I read on Stack Overflow which had me going "oh yeah! (duh)", was in answer to this question:

However I am wondering what should be the HTTP status code is the request sent by the client is valid (DELETE mySite/entity/123) and the entity to delete does not exist.
Because I was facing the same question (and with GET operations too). The answer is the very obvious:

In that case, the service should return an HTTP 404. Strictly speaking, a DELETE or a GET request for a resource that does not exist is not a "valid" request - ie. the client should not re-attempt that request because it will never succeed... The HTTP protocol defines 2 categories of problems - those with a 4xx status code, where the client must modify the request before retrying it, and those with a 5xx status code, which indicate that the service ran into trouble and the client should/could retry the same exact request without changing it.
As I'm perpetually wont to say to people who marvel that REST is some kind of wonderous thing... it's not. It's just "making HTTP requests". We do this every day in our browser. REST requests are no different. So if a resource doesn't exist... 404 the request.

This is all good for DELETE operations, as one just needs to do this:

/**
* @httpmethod        DELETE
* @restPath            {id}
* @id.restargsource    path
*/ 
remote void function deleteById(required numeric id){
    var user = entityLoad("User", id, true)
    if (!isNull(user)){
        entityDelete(user)
        restSetResponse({status=204})
    }
    restSetResponse({status=404})
}

So here I return appropriate status codes for the result of the request: 204 means:
204 No Content
The server successfully processed the request, but is not returning any content. Usually used as a response to a successful delete request.
And 404 is the familiar "not found" response. Cool.

I had finished reading that at 11:40am.

I can use restSetResponse() for the DELETE method because my returntype is void. Which is a restriction of restSetResponse() (not in the docs, but in this DevNet article: "Getting started with RESTful web services in ColdFusion").

But what about my GET method?

Here it is:

/**
* @httpmethod        GET
* @restPath            {id}
* @id.restargsource    path
*/ 
remote User function getById(required numeric id){
    var user = entityLoad("User", arguments.id, true)
    if (!isNull(user)) {
        return user
    }
    restSetResponse({status=404}) //this errors due to stupidity in CFML
}

As noted in the comment... nuh-uh. I cannot do this.

On Railo (that's Railo code above, I realise it won't run on ColdFusion - no semi-colons - but the equivalent in ColdFusion dun't work either), it gets it partially right:


It gets the response code correct, but still actually errors. And on ColdFusion I get this:


It doesn't see fit to pay attention to the response code I asked for, it just follows its own stupid internal rules and goes "nup, you can't do that! I have rules you know!".

Stupid bloody thing.

However I think this finally demonstrates that this rule that restSetResponse() can only work on void functions is just... wrong. There is a very good use case where it needs to work on functions which quite legitimately return something in most situations, but one needs to use restSetResponse() in abnormal situations. Well a 404 isn't even a very "abnormal" situation.

This is where I refer to the bug I raised with Adobe on this (3546046), where Paul offered this up:

  • Paul Nibin K J
    5:25:45 AM GMT+00:00 Jan 3, 2014
    RestSetResponse is not like return statement. RestSetResponse does not actually return anything. It just sets the custom response, which ColdFusion gets and sends the response.

    In ColdFusion, cffunctions should adhere to the return type. If you have specified a return type, you should return some value of that type.
    [etc]
This is kind of the problem. Not returning an object of a given type should not violate the type-restriction on the function. For example this:

string function conditionallyReturnString(required boolean returnAString){
    if (returnAString) {
        return "";
    }
    return;
}

emptyString = conditionallyReturnString(true);

nullString = conditionallyReturnString(false);
writeOutput(isnull(nullString));
writeDump(var=[variables]);

Running this, one gets:

YES
array
1
struct
CONDITIONALLYRETURNSTRING
EMPTYSTRING[empty string]

So it's fine to not return a string from a function which has a type string. For some reason that's the only data type in ColdFusion that follows that rule: I tested numeric, dates, arrays, CFC instances etc, and they all go:

The value returned from the conditionallyReturnArray function is not of type array.

Or some variation on that theme. Railo behaves much the same, except one can also return null from a numeric function too, and it doesn't error.

But WhyTF is CFML - a loosely-typed language - doing this? Java - strongly-typed - thinks its fine to return a null string or null object reference from a method:

public class TestReturningNullObject {

    public static void main(String[] args) {
    }

    public C conditionallyReturnObject(boolean returnAnObject) {
        if (returnAnObject){
            return new C();
        }
        return null;
    }
    
    class C{}

}

o = createObject("java", "TestReturningNullObject");
emptyC = o.conditionallyReturnObject(true);

nullC = o.conditionallyReturnObject(false);
writeOutput(isnull(nullC));
writeDump(var=[variables]);

Output:

YES
array
1
struct
EMPTYC
object of TestReturningNullObject$C
Class NameTestReturningNullObject$C
O

So the problem here really is not with restSetResponse(), I suppose. The problem is that there's this bung rule in CFML that one cannot return null from a function that expects to return an object type. And this is wrong.

However that's a bit more of an architectural change than Adobe are gonna want to do. Even if they alter the rules for REST services wherein the response can quite legitimately take the form of [some data in the response], or just some headers. CFML REST services need to respect this.

On the other hand... I need to get this stuff working. Anyone know a way of being able to write REST services so they are capable of making a correct response for both happy and unhappy paths?

Someone else must've worked around this?


Oh... why was I deliberately giving you the times at which I completed various steps of this investigation?

Because I decided to write this blog article at midday. It's now 6pm and I've been writing for about an hour.

All the time from 12pm - 5pm was simply trying to coerce ColdFusion into actually running this RESTful CFC. It works fine in Railo and took less than an hour to write. To convert it to something ColdFusion would work reliably with took almost 5hrs. The obvious syntax changes took about half an hour to iron out (to get to a point where the could would even compile on ColdFusion 10), the rest of the time was spent going "WTF?"... "FFS, will you work?"... "Oh for goodness sake, what's wrong now?"... "Oh... you need it to be like that do you?"... "um... OK, I'll restart ColdFusion entirely then to see if that helps" (sometimes yes, sometimes no, btw), "but that's just... wrong". Four hours of that. And reading on StackOverflow (because the Adobe docs are a waste of frickin' space) hoping someone else had seen a given error message (generally: no).

The ColdFusion REST implementation has had a bit of a bad rap in this regard, I know. One thing I can say is that it's not a syntactical issue, it's just a bad implementation. Railo is far more stable, doesn't have any unexpected idiosyncracies, and - as long as yer code is syntactically correct - just works (without restarts or reboots too, I hasten to add). I really wonder how Adobe managed to make such a pig's ear of this stuff.

All I was hoping to do today was to do that backbone.js tutorial. I guess I should simply have stuck with the working code in Railo and got on with it, rather than trying to get this code working in CF so I can blog about it. My bad. But now I've been completely sapped of the will to live, so I'm going to grab a glass of wine and be done with it.  I might document the shortfalls I encountered with CF's REST implementation tomorrow. But... I'm not sure I can be arsed.

I will follow-up the CF bug and observe that not being able to use restSetResponse() on methods which aren't void is not a starter, and something needs to be done there.

Where's that wine?

--
Adam