Sunday 31 March 2013

restSetResponse() requires the method to be returntype void. What?

G'day:
I have to say I didn't even know the restSetResponse() function even existed until I read about it on Stackoverflow this afternoon. The Stackoverflow question drew my attention to this snippet in the docs:

You must set the returntype attribute of the function to void when a custom response is returned using the function restSetResponse.

Note that it says no such thing against the docs for the actual function (which would be the most sensible place to mention it), just in this devnet article. I have annotated the docs to point this out.

The Stackoverflow question makes a good point:

It works well, except that it forces you to set the function's returntype to "void". The problem with "void" is that whenever I throw an exception, it no longer returns the proper JSON error message.
And they're right. It's entirely reasonable that a method might only conditionally furnish its response via this restSetRespose(), and otherwise respond by returning stuff (be it an error in this case, or any other sort of response). It makes no sense to me that whether or not restSetResponse() works is based on the return type of the method. That's just daft. Now I can understand that after restSetResponse() is called then any other return from the function might be ignored. Or vice-versa that the content set by restSetResponse() would subsequently be overriden by a return statement. But it should not arbitrarily not work if the returntype isn't void.

To explain what I'm babbling on about, I have some code.

First things first, here's just a bog-standard REST method which returns some JSON, just to set the scene:

component rest=true restPath="responder"  {

    remote array function returnsJson() httpmethod="get" restpath="returnsJson" produces="application/json" {
        return ["tahi", "rua", "toru", "wha"];
    }

}

I call this as follows:

<cfhttp method="get" url="http://localhost:8500/rest/components/responder/returnsJson/" result="response">
<cfdump var="#response#" label="returnsJson()">

And the results are:

returnsJson() - struct
Charset[empty string]
ErrorDetail[empty string]
Filecontent["tahi","rua","toru","wha"]
HeaderHTTP/1.1 200 OK Server: Apache-Coyote/1.1 Content-Type: application/json Content-Length: 27 Date: Sun, 31 Mar 2013 02:58:07 GMT Connection: close
Mimetypeapplication/json
Responseheader
returnsJson() - struct
Connectionclose
Content-Length27
Content-Typeapplication/json
DateSun, 31 Mar 2013 02:58:07 GMT
ExplanationOK
Http_VersionHTTP/1.1
ServerApache-Coyote/1.1
Status_Code200
Statuscode200 OK
TextYES

No surprises.

Next let's see what the bod on Stackoverflow was seeing when throwing an exception when the returntype is struct, and see what's returned.  Here's the method:

remote string function raisesExceptionViaStruct() httpmethod="get" restpath="raisesExceptionViaStruct" produces="application/json" {
    throw(errorcode=400, type="RestException", message="An exception occurred in raisesExceptionViaStruct()");
}

The calling code is much the same as the previous example (and for subsequent examples too), so I'll not repeat it. The output of this one is:

raisesExceptionViaStruct() - struct
Charset[empty string]
ErrorDetail[empty string]
Filecontent{"Message":"An exception occurred in raisesExceptionViaStruct()","Type":"RestException"}
HeaderHTTP/1.1 400 Bad Request Server: Apache-Coyote/1.1 Vary: Accept Content-Type: application/json Content-Length: 88 Date: Sun, 31 Mar 2013 02:58:07 GMT Connection: close
Mimetypeapplication/json
Responseheader
raisesExceptionViaStruct() - struct
Connectionclose
Content-Length88
Content-Typeapplication/json
DateSun, 31 Mar 2013 02:58:07 GMT
ExplanationBad Request
Http_VersionHTTP/1.1
ServerApache-Coyote/1.1
Status_Code400
VaryAccept
Statuscode400 Bad Request
TextYES


I think this is cool! I was expecting the throw() to cause an actual exception, but I like the way ColdFusion rolls it up in a JSON packet, and sets the HTTP response as directed. Incidentally, it's the same with a string return type too.

Let's see a variation of this where the return type is void:

remote void function raisesExceptionViaVoid() httpmethod="get" restpath="raisesExceptionViaVoid" produces="application/json" {
    throw(errorcode=400, type="RestException", message="An exception occurred in raisesExceptionViaVoid()");
}
 
raisesExceptionViaVoid() - struct
Charset[empty string]
ErrorDetail[empty string]
Filecontent[empty string]
HeaderHTTP/1.1 500 [coldfusion.runtime.CustomException : An exception occurred in raisesExceptionViaVoid(). ] Server: Apache-Coyote/1.1 server-error: true Transfer-Encoding: chunked Date: Sun, 31 Mar 2013 02:58:07 GMT Connection: close
MimetypeUnable to determine MIME type of file.
Responseheader
raisesExceptionViaVoid() - struct
Connectionclose
DateSun, 31 Mar 2013 02:58:07 GMT
Explanation[coldfusion.runtime.CustomException : An exception occurred in raisesExceptionViaVoid(). ]
Http_VersionHTTP/1.1
ServerApache-Coyote/1.1
Status_Code500
Transfer-Encodingchunked
server-errortrue
Statuscode500 [coldfusion.runtime.CustomException : An exception occurred in raisesExceptionViaVoid(). ]
TextYES

It's predictable that there's no serialised error message in the content, because of the void return type. But it seems a bit rubbish that the errorCode I specified in the throw() hasn't been respected. I can't see the sense in that.

This demonstrates, though, what the person on Stackoverflow was talking about: when the return type is void, we lose control over how the error is reported.

So now let's look at restSetResponse() in action:

remote void function returnsViaRestSetResponseUsingVoid() httpmethod="get" restpath="returnsViaRestSetResponseUsingVoid" produces="application/json" {
    restSetResponse({
        status    = "202",
        content    = "Content set by returnsViaRestSetResponse()",
        headers    = {
            date    = dateTimeFormat(now(), "full"),
            from    = "notMyAddress@example.com",
            warning    = "Just testing!!"
        }
    });
}

And the response:

returnsViaRestSetResponseUsingVoid() - struct
Charset[empty string]
ErrorDetail[empty string]
FilecontentContent set by returnsViaRestSetResponse()
HeaderHTTP/1.1 202 Accepted Server: Apache-Coyote/1.1 WARNING: Just testing!! FROM: notMyAddress@example.com DATE: Sun, 31 Mar 2013 02:58:07 GMT Content-Type: application/json Transfer-Encoding: chunked Connection: close
Mimetypeapplication/json
Responseheader
returnsViaRestSetResponseUsingVoid() - struct
Connectionclose
Content-Typeapplication/json
DATESun, 31 Mar 2013 02:58:07 GMT
ExplanationAccepted
FROMnotMyAddress@example.com
Http_VersionHTTP/1.1
ServerApache-Coyote/1.1
Status_Code202
Transfer-Encodingchunked
WARNINGJust testing!!
Statuscode202 Accepted
TextYES


Because we have specified void for the return type, the function works, demonstrating it's quite handy. However now I adjust the method to have returntype of string (but not changing it other than that), and it stops working:

remote string function returnsViaRestSetResponseUsingString() httpmethod="get" restpath="returnsViaRestSetResponseUsingVoid" produces="application/json" {
    restSetResponse({
        status    = "202",
        content    = "Content set by returnsViaRestSetResponse()",
        headers    = {
            date    = dateTimeFormat(now(), "full"),
            from    = "notMyAddress@example.com",
            warning    = "Just testing!!"
        }
    });
}

returnsViaRestSetResponseUsingString() - struct
Charset[empty string]
ErrorDetail[empty string]
Filecontent[empty string]
HeaderHTTP/1.1 200 OK Server: Apache-Coyote/1.1 Content-Type: application/json Content-Length: 0 Date: Sun, 31 Mar 2013 04:32:33 GMT Connection: close
Mimetypeapplication/json
Responseheader
returnsViaRestSetResponseUsingString() - struct
Connectionclose
Content-Length0
Content-Typeapplication/json
DateSun, 31 Mar 2013 04:32:33 GMT
ExplanationOK
Http_VersionHTTP/1.1
ServerApache-Coyote/1.1
Status_Code200
Statuscode200 OK
TextYES

I think this is rubbish. There's no way a function should just not do anything. If it's illegal to call this function from within a function which is not return type void, then an exception should be raised. However it's such a stupid requirement, it should be removed.

Here's a very contrived function that demonstrates a method which might raise an exception, or might complete correctly, and then wish to use restSetResponse() to return the response:

remote void function methodThatPossiblyRaisesException(required boolean throwException restargsource="path") httpmethod="get" restpath="methodThatPossiblyRaisesException/{throwException}" produces="application/json" {
    if (throwException){
        throw(errorcode=400, type="RestException", message="An exception occurred in methodThatPossiblyRaisesException()");
    } else {
        restSetResponse({
            status    = "202",
            content    = "Content set by returnsViaRestSetResponse()",
            headers    = {
                date    = dateTimeFormat(now(), "full"),
                from    = "notMyAddress@example.com",
                warning    = "Just testing!!"
            }
        });
    }
}

(I told you it was contrived, but you can see what I'm getting at). And unsurprisingly - based on what I showed above - the error condition does not fulfill its intent:

methodThatPossiblyRaisesException(false) - struct
Charset[empty string]
ErrorDetail[empty string]
FilecontentContent set by returnsViaRestSetResponse()
HeaderHTTP/1.1 202 Accepted Server: Apache-Coyote/1.1 WARNING: Just testing!! FROM: notMyAddress@example.com DATE: Sun, 31 Mar 2013 04:40:23 GMT Content-Type: application/json Transfer-Encoding: chunked Connection: close
Mimetypeapplication/json
Responseheader
methodThatPossiblyRaisesException(false) - struct
Connectionclose
Content-Typeapplication/json
DATESun, 31 Mar 2013 04:40:23 GMT
ExplanationAccepted
FROMnotMyAddress@example.com
Http_VersionHTTP/1.1
ServerApache-Coyote/1.1
Status_Code202
Transfer-Encodingchunked
WARNINGJust testing!!
Statuscode202 Accepted
TextYES

methodThatPossiblyRaisesException(true) - struct
Charset[empty string]
ErrorDetail[empty string]
Filecontent[empty string]
HeaderHTTP/1.1 500 [coldfusion.runtime.CustomException : An exception occurred in methodThatPossiblyRaisesException(). ] Server: Apache-Coyote/1.1 server-error: true Transfer-Encoding: chunked Date: Sun, 31 Mar 2013 04:40:23 GMT Connection: close
MimetypeUnable to determine MIME type of file.
Responseheader
methodThatPossiblyRaisesException(true) - struct
Connectionclose
DateSun, 31 Mar 2013 04:40:23 GMT
Explanation[coldfusion.runtime.CustomException : An exception occurred in methodThatPossiblyRaisesException(). ]
Http_VersionHTTP/1.1
ServerApache-Coyote/1.1
Status_Code500
Transfer-Encodingchunked
server-errortrue
Statuscode500 [coldfusion.runtime.CustomException : An exception occurred in methodThatPossiblyRaisesException(). ]
TextYES

Same as with the earlier method raisesExceptionViaVoid(): the error is not reported correctly when the return type is void. And if I make the return type string / struct, then the restSetResponse() call doesn't work. So one cannot get both responses to behave correctly: it's one or the other.

I've kinda been able to work around this, as follows:

remote void function methodThatPossiblyRaisesExceptionWorkAround(required boolean throwException restargsource="path") httpmethod="get" restpath="methodThatPossiblyRaisesExceptionWorkAround/{throwException}" produces="application/json" {
    var response = {
        headers    = {
            date    = dateTimeFormat(now(), "full"),
            from    = "notMyAddress@example.com"
        }
    };
    if (throwException){
        response.status            = "400";
        response.content        = serializeJson({type="RestException", message="An exception occurred in methodThatPossiblyRaisesException()"});
        response.headers.warning= "It errored";
    } else {
        response.status         = "202";
        response.content        = "It was OK";
        response.headers.warning= "No warning";
    }
    restSetResponse(response);
}

Note how I don't actually throw an exception here, I just fake it, and set the headers accordingly, then using restSetResponse() to pass this correct response back to the calling code.  A variation of this could be implemented to try/catch code, and then pass the caught exception's details back via the same mechanism. Here's the output of both options (throwException=false, and throwException=true):

methodThatPossiblyRaisesExceptionWorkAround(false) - struct
Charset[empty string]
ErrorDetail[empty string]
FilecontentIt was OK
HeaderHTTP/1.1 202 Accepted Server: Apache-Coyote/1.1 WARNING: No warning FROM: notMyAddress@example.com DATE: Sun, 31 Mar 2013 04:40:23 GMT Content-Type: application/json Transfer-Encoding: chunked Connection: close
Mimetypeapplication/json
Responseheader
methodThatPossiblyRaisesExceptionWorkAround(false) - struct
Connectionclose
Content-Typeapplication/json
DATESun, 31 Mar 2013 04:40:23 GMT
ExplanationAccepted
FROMnotMyAddress@example.com
Http_VersionHTTP/1.1
ServerApache-Coyote/1.1
Status_Code202
Transfer-Encodingchunked
WARNINGNo warning
Statuscode202 Accepted
TextYES

methodThatPossiblyRaisesExceptionWorkAround(true) - struct
Charset[empty string]
ErrorDetail[empty string]
Filecontent{"MESSAGE":"An exception occurred in methodThatPossiblyRaisesException()","TYPE":"RestException"}
HeaderHTTP/1.1 400 Bad Request Server: Apache-Coyote/1.1 WARNING: It errored FROM: notMyAddress@example.com DATE: Sun, 31 Mar 2013 04:40:23 GMT Content-Type: application/json Transfer-Encoding: chunked Connection: close
Mimetypeapplication/json
Responseheader
methodThatPossiblyRaisesExceptionWorkAround(true) - struct
Connectionclose
Content-Typeapplication/json
DATESun, 31 Mar 2013 04:40:23 GMT
ExplanationBad Request
FROMnotMyAddress@example.com
Http_VersionHTTP/1.1
ServerApache-Coyote/1.1
Status_Code400
Transfer-Encodingchunked
WARNINGIt errored
Statuscode400 Bad Request
TextYES


This time, both options work. but this is a crappy work around, and there should be no reason (that I can see) that one should have to do this.

Am I missing something? I suspect not, because if one googles restSetResponse(), one first get its docs, then a series of questions on Stackoverflow of people asking "WTF?". So it seems my expectations are mirrored by other people. Is there a good reason why restSetResponse() works the way it does, needing the method its within to have a return type of void?  If you know, please do tell...

Update:
I've raised a bug for this behaviour: 3546046

Righto.

--
Adam