Friday, 18 January 2013

ColdFusion: makes hard things easy easy things incomprehensible

G'day:
Now this article is being true to the idea of a blog being a "web log". It's not very interesting, but it logs the high pointnadir of my day, yesterday. You will learn very little from reading this article. If that doesn't sell it to you, I don't know what will!

I got undone by how ColdFusion reports exception types.

Yesterday I was having a curious time trying to work out how to unit test some fairly complex code without having to spend hours mocking stuff so that the method would run to the point I needed to test. I'll not bore you with the details of that, but I had a situation where I had mocked a method called within the function I was testing so that it would error out, so as as to bypass the rest of the method which I wasn't interested in. I was just wanting to see the call log of the method I was making error. To make the method error, I simply mocked it to return the incorrect data type, which yielded an exception I could catch, and then check the call log etc.  The thing was, try as I might (get the pun there?), I could not catch the damned exception it was raising. Now this was not because it forced a compile error or something like that that cannot be caught, it was a runtime exception, and I simply could not catch it by type. I could use an "any" catch, but I think that's sloppy, plus I wanted to know what was going on. And I ended up finding a curious bug in ColdFusion.

I've distilled the issue down to some sample code. Firstly, here's some code that demonstrates catching a specific exception type:

try {
    throw(type="MyCustomException");    // this throws a MyCustomException...
}
catch("MyCustomException" e){    // ...and this catches it
    writeOutput("Exception type from exception object: #e.type#<br />"); // this confirms the type is indeed MyCustomException
}

That's straight fwd. And it outputs:

Exception type from exception object: MyCustomException

Right, so now I emulate the environment in my (failed) test code:

void function f(required numeric x){}    // this is just used to force the exception when called incorrectly
try {
    f("not a numeric");    // a string the wrong type passed in here, so we expect an exception
}
catch(any e){    // do a catch-all so we can check the exception type
    writeOutput("Exception type from exception object: #e.type#<br />");    // this outputs "numeric", suggesting the exception type is "numeric"
}

Here I've got a function which expects a numeric, and I pass it a string. This errors (as expected... and in this case "as desired"), and the output is:

Exception type from exception object: numeric

Right, so from this I can infer that if I want to catch that exception, I catch type "numeric":

try {
    f("not a numeric");    // a string is the wrong type passed in here, so we expect an exception
}
catch("numeric" e){    // according to the previous test, the exception type is "numeric", so this should work...
    writeOutput("numeric exception caught<br />");
}
catch(any e){    // ... but it didn't, so do a catch-all so we can check the exception type
    writeOutput("numeric exception NOT caught<br />");
    writeOutput("Exception type from exception object: #e.type#<br />");    // but this still outputs "numeric"
}

One would expect to get "numeric exception caught" here, but instead I get:

numeric exception NOT caught
Exception type from exception object: numeric

Right, so it's an exception of type "numeric" but a catch for exceptions of type "numeric" doesn't work.

[exasperated stare]

Obviously ColdFusion is not to be trusted here, so I roll my sleeves up and write some more interrogative code, to see what's going on.

try {
    f("not a numeric");    // a string is the wrong type passed in here, so we expect an exception
}
catch(any e){    // do a catch-all so we can check the exception type
    writeOutput("Exception type from exception object: #e.type#<br />");    // this still outputs "numeric"
    writeDump(e);    // and let's look at the whole thing
    writeOutput(e.getClass().getName());    // this tells us coldfusion.runtime.UDFMethod$InvalidArgumentTypeException
}

And here's the output:

Exception type from exception object: numeric
struct
DetailIf the component name is specified as a type of this argument, it is possible that either a definition file for the component cannot be found or is not accessible.
MessageThe X argument passed to the f function is not of type numeric.
StackTrace
TagContext
Typenumeric
argX
functionNamef
typenumeric
coldfusion.runtime.UDFMethod$InvalidArgumentTypeException

So there's some interesting stuff here:
  • there are two "type" keys, both claiming the type is numeric.
  • Java reckons the actual type is something completely different.
Finally I refactored the code to catch that exception type:

try {
    f("not a numeric");    // a string is the wrong type passed in here, so we expect an exception
}
catch("coldfusion.runtime.UDFMethod$InvalidArgumentTypeException" e){        // using the type returned from the Java interrogation
    writeOutput("Exception type from CF exception object: #e.type#<br />");    // this still outputs "numeric"
    writeOutput("Exception type from Java object: #e.getClass().getName()#<br />");    // this still outputs coldfusion.runtime.UDFMethod$InvalidArgumentTypeException
}

And, indeed this outputs:

Exception type from CF exception object: numeric
Exception type from Java object: coldfusion.runtime.UDFMethod$InvalidArgumentTypeException

So it's still confused as to what the actual type is, but at least the Java exception is catchable.



In the end, I did the code entirely differently than how I was attempting to do it here, and it was a much better approach (thanks to my colleague Brian for the brainstorm there).

But it took about 2h to work out work out what the hell was going on, and it's at least slightly "interesting".

I'm gonna raise a bug regarding the incorrect reporting of the exception type here (3485286).

--
Adam