Friday 29 November 2013

CFML: <cfexit> doesn't quite behave how it's been documented to

G'day:
<cfexit> isn't a tag I have call to use very often. Its primary use is in custom tags, and our coding standard at work prohibits the use of custom tags (I did not write the coding standard, and I do not agree with this position, but hey). I looked at the <cfexit> tag's use within custom tags ages ago, when I first started this blog (Custom Tags: Looping).
As far as I know <cfexit> has never quite behaved the way it is documented to, when used outside of a custom tag. The docs claim that outside of a custom tag, it acts like a <cfabort>, ie: aborting processing altogether. I have never known it to behave this way; what it instead does is simply exit the current file. However processing continues in any "outer" files that had called in the exited one. Huh? I mean like this:

<!--- testExitOuter.cfm --->
Top of testExitOuter.cfm<br>
<cfinclude template="testExitInner.cfm">
Bottom of testExitOuter.cfm<br>

<!--- testExitInner.cfm --->
Top of testExitInner.cfm<br>
<cfexit>
Bottom of testExitInner.cfm<br><!--- should never get to this --->

On all three of ColdFusion, Railo and OpenBD, the behaviour here is the same:


Top of outer.cfm
Top of inner.cfm
Bottom of outer.cfm

Note how inner.cfm has exited, but outer.cfm continues after the include of inner.cfm.

So the ColdFusion docs are plain wrong here. Railo's docs are similar to (but much more sparse than ~) ColdFusion's. I could not find OpenBD's version googling "OpenBD docs cfexit", and not seeing it in the first page of results. If OpenBD can't manage that level of SEO, I can't be arsed hunting around.

There is one situation in which Railo & OpenBD vary in behaviour from ColdFusion:

// Test.cfc
component {

     public void function testExit(){
          writeOutput("Top of testExit()<br>");
          exit;
          writeOutput("Bottom of testExit()<br>");
     }

}

// testExitInFunction.cfm
writeOutput("Top of testExitInFunction.cfm<br>");
o = new Test();
o.testExit();
writeOutput("Bottom of testExitInFunction.cfm<br>");

Here ColdFusion's behaviour is internally consistent: <cfexit> simply exits the "file", in this case, the function:

Top of testExitInFunction.cfm
Top of testExit()
Bottom of testExitInFunction.cfm

However on Railo and OpenBD, suddenly the behaviour is as documented:

Top of testExitInFunction.cfm
Top of testExit()

See how execution has just stopped completely, same as a <cfabort>. Well... almost the same as <cfabort>. Let's see how code runs if we use <cfabort> instead, and also add in some application event handlers: onRequestEnd() and onAbort(). Here we get divergent behaviour on all three platforms.

// Application.cfc
component {

     public void function onRequestEnd(){
          writeOutput("onRequestEnd() called<br>");
     }

     public void function onAbort(){
          writeOutput("onAbort() called<br>");
     }

}

The test code is the same as above, except I've got different variations one with <cfabort> / abort, and one with <cfexit> / exit.

testExitOuter.cfm

ColdFusion:

Top of testExitOuter.cfm
Top of testExitInner.cfm
Bottom of testExitOuter.cfm
onRequestEnd() called

Railo:

Top of testExitOuter.cfm
Top of testExitInner.cfm
Bottom of testExitOuter.cfm
onRequestEnd() called

OpenBD:

Top of testExitOuter.cfm
Top of testExitInner.cfm
Bottom of testExitOuter.cfm
onRequestEnd() called

All three behave the same here. And all three contravene the docs (not that I could actually find the docs for OpenBD... so let's say it contravene the ColdFusion docs).

testAbortOuter.cfm

ColdFusion:

Top of testAbortOuter.cfm
Top of testAbortInner.cfm
onAbort() called

Railo:

Top of testAbortOuter.cfm
Top of testAbortInner.cfm
onAbort() called

OpenBD:

Top of testAbortOuter.cfm
Top of testAbortInner.cfm

It seems OpenBD doesn't have the concept of onAbort(). ColdFusion and Railo both behave as expected here.

testExitInFunction.cfm

ColdFusion:

Top of testExitInFunction.cfm
Top of testExit()
Bottom of testExitInFunction.cfm
onRequestEnd() called

Railo:

Top of testExitInFunction.cfm
Top of testExit()
onRequestEnd() called

OpenBD:

Top of testExitInFunction.cfm
Top of testExit()

Here OpenBD seems to do the equivalent of <cfabort>, so matches the docs. ColdFusion behaves uniformly with its behaviour in a CFM file, but contrary to the docs: exit just exits the current file, but processing continues in the calling calling. Railo seems buggy here: exit seems to behave like an abort, except onAbort() is not fired. Whichever way one spins it: this is not correct behaviour.

testAbortInFunction.cfm

ColdFusion:

Top of testAbortInFunction.cfm
Top of testAbort()
onAbort() called

Railo:

Top of testAbortInFunction.cfm
Top of testAbort()
onAbort() called

OpenBD:

Top of testAbortInFunction.cfm
Top of testAbort()

Everything behaves to spec here, given OpenBD doesn't support onAbort(). So that's something.

Conclusion

ColdFusion

I think the behaviour of ColdFusion here is "correct", despite it disagreeing with the docs. It follows the docs as far as custom tags go, and it follows the closest analogy to that when called in other places. We don't need <cfexit> to behave like <cfabort>, because we've got <cfabort> for that.

Railo

I think the need to sort out their behaviour here to either follow ColdFusion, or follow the docs. They're doing neither at the moment: if <cfexit> is supposed to behave like <cfabort>, then onAbort() should be being called, not onRequestEnd(). But the most sensible approach would be to follow the behaviour of ColdFusion here. Not because it should be following ColdFusion, per se, but because CF is taking the most sensible route already.

OpenBD

Seems to follow Railo, but have forgotten to implement onAbort(). Or decided not to. Either way: not ideal.

That's it.

--
Adam

<cfexit>