CFAbort And OnRequestEnd() Behavior In ColdFusion 8 And ColdFusion 9
A couple of months ago, I blogged about changes in ColdFusion 9 that allowed the onRequestEnd() event handler to be called after a CFLocation tag. In the comments to that post, David Boyer brought it to my attention that, as of ColdFusion 9, the CFAbort tag worked the same - that is, that it allowed the onRequestEnd() event to execute. A CFLocation tag I can understand; but, a CFAbort tag? This seems to fly in the face of the very intent of an "abort" action. I had to see this with my own eyes.
To test this behavior, I set up a simple Application.cfc ColdFusion framework component and an index file. The Application.cfc simply logs the existing event handlers; the index.cfm file aborts the current request. Let's look at the Application component first:
Application.cfc - ColdFusion Framework Component
<cfcomponent output="false" hint="I define the application settings and event handlers."> <!--- Define the application settings. ---> <cfset this.name = hash( getCurrentTemplatePath() ) /> <cfset this.applicationTimeout = createTimeSpan( 0, 0, 5, 0 ) /> <!--- Define the local file path for the logging. ---> <cfset this.logFilePath = (getDirectoryFromPath( getCurrentTemplatePath() ) & "log.txt") /> <!--- Determine the ColdFusion engine prefix to use in the logging (so we can run this on ColdFusion 8 and ColdFusion 9). ---> <cfif find( "cf8", cgi.server_name )> <!--- ColdFusion 8 configuration. ---> <cfset this.engine = "CF8.0.1" /> <cfelse> <!--- ColdFusion 9 configuration. ---> <cfset this.engine = "CF9.0.1" /> </cfif> <cffunction name="onRequestStart" access="public" returntype="boolean" output="false" hint="I initialize the request"> <!--- Log the request start. ---> <cffile action="append" file="#this.logFilePath#" output="#this.engine# - onRequestStart" addnewline="true" /> <!--- Return true so the request can load. ---> <cfreturn true /> </cffunction> <cffunction name="onRequest" access="public" returntype="void" output="true" hint="I execute the requested script."> <!--- Log the request. ---> <cffile action="append" file="#this.logFilePath#" output="#this.engine# - onRequest - PRE Content" addnewline="true" /> <!--- Include the requested script. ---> <cfinclude template="#arguments[ 1 ]#" /> <!--- Log the request. ---> <cffile action="append" file="#this.logFilePath#" output="#this.engine# - onRequest - POST Content" addnewline="true" /> <!--- Return out. ---> <cfreturn /> </cffunction> <cffunction name="onRequestEnd" access="public" returntype="void" output="false" hint="I teardown the request."> <!--- Log the request start. ---> <cffile action="append" file="#this.logFilePath#" output="#this.engine# - onRequestEnd" addnewline="true" /> <!--- Return out. ---> <cfreturn /> </cffunction> <cffunction name="onError" access="public" returntype="void" output="false" hint="I handle any uncaught exceptions."> <!--- Log the error. ---> <cffile action="append" file="#this.logFilePath#" output="#this.engine# - onError" addnewline="true" /> <!--- Return out. ---> <cfreturn /> </cffunction> </cfcomponent>
As you can see, this code basically just logs the event handler names to a text file. I am setting up an engine prefix for the logging so we can see which ColdFusion version is being used - I'm running this in both 8.0.1 and 9.0.1.
The onRequest() event handler includes the currently requested script, which in this test, is always index.cfm. The index.cfm is the template that uses the CFAbort tag.
<!--- This is the main index file. ---> Hello :) <!--- Kill the current request. ---> <cfabort />
Ok, so this is all pretty simple. When we run this code in ColdFusion 8 and then in ColdFusion 9, we get the following log output:
CF8.0.1 - onRequestStart
CF8.0.1 - onRequest - PRE Content
CF9.0.1 - onRequestStart
CF9.0.1 - onRequest - PRE Content
CF9.0.1 - onRequestEnd
As you can see, the onRequestEnd() event handler was invoked in ColdFusion 9 but not in ColdFusion 8. I really don't know how I feel about this. My gut feeling is that it is a mistake. After all, why would someone use a CFAbort tag? It's not a normal kind of control flow - you use CFAbort when you truly don't want anything else on the page to execute (as opposed to using CFExit or CFReturn). And yet, in ColdFusion 9, the onRequestEnd() tag will be invoked.
While the default behavior is divergent between these two releases of ColdFusion (8 and 9), you can still get them to act the same way by using the CFAbort tag's showError attribute. When you add the showError attribute, the CFAbort tag works more like a CFThrow tag, raising an exception.
Let's update our index.cfm file to use the showError attribute:
Index.cfm (With CFAbort ShowError Attribute)
<!--- This is the main index file. ---> Hello :) <!--- Kill the current request. ---> <cfabort showerror="Noooooo!" />
Now, when we invoke this in ColdFusion 8 and then in ColdFusion 9, we get the following log output:
CF8.0.1 - onRequestStart
CF8.0.1 - onRequest - PRE Content
CF8.0.1 - onError
CF9.0.1 - onRequestStart
CF9.0.1 - onRequest - PRE Content
CF9.0.1 - onError
As you can see, neither of the ColdFusion engines invoke the onRequestEnd() event handler; however, they now both invoke the onError() event handler to catch the CFAbort-based exception. As such, you'd have to put some control flow logic within your onError() event handler (to not treat CFAbort exceptions as true "errors").
It's probably best to avoid the CFAbort tag in general. The ColdFusion application framework gives you some great hooks to change the way requests are processed (such as returning False from the onRequestStart() event handler). But, that said, I still don't think the new CFAbort behavior in ColdFusion 9 is good; it's outcome does not appear to be in alignment with the intent of the tag.
Want to use code from this post? Check out the license.
Thanks for blogging about this ;)
The work around we ended up using where I work involved setting a request variable (e.g. request.complete = true) as the last line of onRequest. Then checking for it in onRequestEnd, if it doesn't exist then cfabort (again!). Of course that only works if you're using onRequest :)
I agree with you though, it doesn't feel right. How else other than cfabort are you supposed stop things processing?
That's a pretty good work around. My guess is, they had to add this for some other reason and didn't necessarily catch this as a byproduct.
@Ben, as another workaround, where appropriate:
The original Application.cfm and OnRequestEnd.cfm approach still behaves the CF 8 way. CFAbort just stops processing, and OnRequestEnd.cfm is not included. Not sure about CFLocation, but I suspect that also doesn't go through OnRequestEnd.cfm.
This breaks backwards compat, so should be considered a bug, yes?
Equally, the docs for CFLOCATION state that it "Stops execution of the current page", so if that's not what it does, that's a bug too.
If they want to change the behaviours for these tags (I see an argument for this), then perhaps an optional parameter, eg:
Or something like that. But the DEFAULT behaviour should reflect previous behaviour, and indeed the intent of the tag. Something called CFABORT should abort. not "kinda abort a bit, but not really".
Oh for goodness sake.
It's probably due to this:
That was a dumb E/R.
WTF!? cfabort should not fire onRequestEnd.
A "graceful" attribute could be handy, but it should certainly not be the default behaviour.
All the people that voted for and implemented this behaviour need a big slap!
Thanks for posting Ben, this has been puzzling me for a while.
Pretty big problem IMHO.
Ahhh, good find on the Bug Tracker. I wonder what kind of issues Mark had when he suggested it. I would assume he *did* have some valid use case - Mark is a pretty smart dude.
That said, I still think it's a bug :D
And, if you put a CFAbort in the Application.cfc pseudo constructor, neither the onRequestStart() nor the onRequestEnd() fires. If you're going to have logic that say "onRequestEnd() should fire no matter what," wouldn't is *also* make sense that onRequestStart() would also follow that same logic? After all, a request is a request is a request.
I think the bug tracker item came from the idea that page processing is not the same as request processing. Although I didn't know about the bug tracker item, I probably would have voted for it, since it never made sense to me that aborting the page processing would also end the request without firing the "request-end" event.
I would think every request should fire the start event and end event, regardless of what occurs in the middle. It really doesn't make sense to me to say "Runs at the end of a request, after all other CFML code (unless the processing of a ColdFusion page ends early)."
I've raised a bug for this to get this change backed-out:
Pls consider voting for it.
Mike, whether or not it makes sense, changing the behaviour of this tag in such a way will break many applications, and Adobe always say they take backwards compatibility into account.
There are no applications prior to CF9 that depend on onRequestEnd firing with cfabort, but there will be plenty that depend on it not firing, that now require extra work done.
In Railo, there is a `type` attribute on cfabort which defaults to "request" but can be changed to "page" to act as you describe.
This has been there since Railo 2 (i.e. before CF8 was released), and it would have made far more sense to implement that attribute than to change the behaviour of the tag.
Yep, I would possibly agree if that's the way onRequestEnd() was initially implemented; but it wasn't.
Equally one could - slightly semantically - that a CFABORT condition is not the request *ending*, it's the request being aborted. Not the same thing. Then again I've always thought CFABORT's behaviour of ABORTing *and* flushing the buffer was a daft idea. As soon as a piece of functionality does x AND y, then it's a bad implementation.
If you read my bug entry (as per previous comment), you'll see my position is that CFABORT should *abort*. Immediately. This was always its intent.
If we want to quit processing, then that's more the realm of CFEXIT. A precedent has been set there for that sort of behaviour, so extending it to also deal with exiting the request makes sense.
However one looks at it, what SHOULDN'T have happened is the default behaviour of CFABORT being changed.
Sounds like to be safe, you'd need to change all CFAborts that are in your code to a new function that you create that would pass a variable to "onRequestEnd" that says, "Graceful = false"
<cfif graceful eq false> </cfif>
If you are not currently using the "showerror" attribute for cfabort, you can update all cfabort tags to set this, and then in your onError function you can do:
If you currently use the showerror attribute for other purposes, you would probably need to do something like what you say...
Simplest one is probably to replace
and then inside onRequestEnd do
(And of course, if you've got any cfscript with abort keyword used, you'd have to do an equivalent replace for that too.)
They could also resolve the issue by adding a second argument to the onRequestEnd method which indicates whether or not the request was ended abruptly--like via CFABORT.
That way you could easily add logic to your onRequestEnd() method if you wanted alternative logic for requests that didn't end normally.
I wasn't really referring to backwards compatibility. I consider maintaining backwards compatibility a separate issue. Adobe should have done one of two things for that:
1) Maintained backwards compatibility by default and provide the option for the new functionality (basically how everyone had suggested).
2) Made it more apparent that this functionality was changing so it could be tested before people upgraded to the new version. I realize people should be testing that anyway, but I'm fairly certain nobody would know to test to make sure cfabort wasn't breaking their applications unless it was highlighted better.
The first way would have been much better. However, the change to cfabort is similar to what they did with listToArray() and empty list items, and they didn't preserve backwards compatibility by default for that either, so I'm not really surprised. I am surprised they didn't provide a way in the tag or the statement (for script syntax) a way to preserve backwards compatibility.
But it doesn't really change my opinion that they were right to go this route (even if their implementation of the change was flawed).
My position is that cfabort should not do anything to the request, but should abort the page processing. I consider the page processing to be separate from the request.
Looking at the livedocs, Adobe uses different terminology when decribing how the request event handlers function vs cfabort. They don't say cfabort should abort the request, they say it "Stops the processing of a ColdFusion page at the tag location." The cfexit tag doesn't really apply either since according to the livedocs that's to end execution of custom tag processing.
This is why in my previous comment I mentioned that the bug comes from the idea that page processing != request processing. I think the difference in opinion comes down to a difference in interpretation of what is in the documentation. There isn't anywhere I've seen where they've explicitly related the two or not.
The documentation is wrong - prior to CF9, aborting the request is exactly what it did (as proven by it not executing onRequestEnd method nor OnRequestEnd.cfm template).
Documentation should always be corrected to match actual behaviour on a released product, (even if the plan is to then change that behaviour in the next release).
I'm not sure what you're referring to with ListToArray - they added a new argument with CF8 and another one with CF9, but in both cases the default behaviour has not changed. Empty list items have always been ignored, and still are (assuming the docs are not incorrect here).
Dan: We've already got onError and cfthrow to do that?
The way I would likely have solved this problem is to add an onFinally method that would always run last, whether the request ended with onRequestEnd, onError, cfabort, or any other way of stopping processing that doesn't go down these channels. The first argument to that method would be which route it went down, and the remaining arguments would be the parameters sent into the previous method (or the attributes to cfabort if that was how it ended).
But I think the key thing which is annoying/perplexing me and Adam (and others) is that Adobe silently changed behaviour without (afaik) telling anyone - if it wasn't for David & Ben's discovery, we'd still be unaware - and if anyone had asked "does onRequestEnd run after cfabort?" I'd have responded with a firm no, because it's something I've explicitly tested in the past and the type of change that shouldn't happen without notification.
Hi again Mike
Clearly the intent of CFABORT, irrespective of the wording of the docs, is to abort the REQUEST, not the "page" (I wish Adobe et al would stop referring to a CF file as a "page". It sounds like something someone who would say "the internets" would say. But anyway). If CFABORT terminated processing of the CFM file it was in, then if that file was included by a "parent" file, one would expect processing to resume in the parent file after the CFINCLUDE line. Like how CFEXIT does. Indeed that they actually have CFEXIT for this kinda demonstrates that is specifically NOT what CFABORT is for.
BTW, the docs for CFEXIT are wrong. Well: they're right in the context of custom tag execution, but they're wrong when it comes to how it works within the "normal" execution flow of CF files. CFEXIT stops execution of the current *file*, and skips out to the file that called that one. If the CFEXIT is in the "outermost" file, then it exits and that's the end of processing. Other than... cough... onRequestEnd() also processing. But, TBH, I think that's the correct thing to do in this case.
Thinking more about this CFABORT / CFEXIT thing... the original change was unnecessary as we've already got CFEXIT to effect the intent of the original enhancement request.
Adam, the concepts of Request, Page, and Template are actually three distinct things.
Request I think we can agree as being the moment CF gets tapped on the shoulder by the web server, up until it delivers its response and waves goodbye.
And Template is just a way of saying "file", or possibly "file that isn't a cfc", depending how you see it.
A Page is kinda of a wrapper for a Template which comes from the JEE side of CF:
Ah, here we go, the comments to one of Ben's previous entries describes it in a nice way:
As for cfexit already being a solution - well, if you have a series of included pages, there's no way to break the execution of all of them without messy logic in each one, so a cfexit method="exitRequest" which immediately stops and calls onRequestEnd would be a sensible enhancement.
I'm not so sure about the JSP analogy you make there. Obviously it's absolutely on the button now that CF runs atop of Java, but CF's usage of the term "page" predates this. It's short for "web page" in the CF usage.
It was perhaps relevant back when the approach to making CF-driven websites was to have one CFM file per web page "template" (hence the term "template" in CF parlance too), but it's not relevant these days now that that's not really the (recommended) approach CF uses.
If anything the JSP terminology is a hang-over from that too. Not CF's specific usage, but of web page construction in general "back then".
But that's a digression.
You're spot on about the CFEXIT thing though. I didn't think of that. However it does demonstrate that the requirement would be more easily solved by enhancing CFEXIT, not monkeying with CFABORT.
Out of curiosity, I'm curious if there are any other tags that this has affected. The only other one that springs to mind would be CFContent when used in certain ways. Doesn't that stop the page processing when it's done?
Why don't you try it and find out! (And then maybe report back...)
Thanks for the bug submission :)
Along those lines, what could be interesting is an onAbort() event handler in the Application.cfc. Then, the programmer could have the option to turn around and invoke the onRequestEnd() event handler if they wanted to.
It's somewhat like when we (speaking generally) invoke onApplicationStart() manually from the onRequestStart() event handler if the app needs to be reset in someway based on a trigger.
So far, the one's I've tested are just CFLocation and CFAbort. In the past, I have seen that CFContent does stop processing in the *same* file:
... of course, none of these are about the same file - they are about the onRequestEnd() event handler. I can run a test on this; now I'm curious.
It's good to see that issue accumulating votes. I dunno whether Adobe actually pay attention to these things, but at least as a community, we're doing our bit to improve CF.
Now... I was thinking about the mention of CFCONTENT on this thread. The more I think about it, the less I'm inclined to think there's a good reason to expect CFCONTENT to NOT fire onRequestEnd().
I'd be keen to hear why people think it shouldn't.
I'll agree with you on CFContent. I think CFAbort is really the only one that I think feels completely wrong.
If CFABORT behavior does not get rolled back to pre-9, perhaps we need a new tag? Something like CFDEAD, CFKILL, CFDONE, CFEND, CFTERMINATE or my favorite CFSTOP...???
Hey Ben! Thanks for posting this. I was struggling with an issue I was sure was a bug in CF9 where I would get a dump of a request variable and then on the very same line as the CFDUMP tag, I would get an error saying that the variable wasn't defined in the request. It turned out actually to be this change of behavior.
I had previously arranged my framework to do basically the same thing (for reasons I won't get into right now ;P) and so now the onRequestEnd was executing twice. Executing twice normally wouldn't be such a great thing anyway, but I had also cleared the request scope at the end of the event to help resolve a previous issue with memory leaks in an earlier version of CF. D'oh! So that was causing the weird "here it's NOT" error messages.
The whole process of debugging it is on my blog here: http://ontap.riaforge.org/blog/index.cfm?mode=entry&entry=58993158-CFC0-A2D4-EB631D225F064EDD
Does anyone ave recent bug base links from the above comment thread? Now, that the Flex version of the bugbase isn't used, all those links are dead, and it's bloody impossible to search the new bugbase for the original bug id.
Hi Brad / Ben
The URL on the new tracker is this: https://bugbase.adobe.com/index.cfm?event=bug&id=3040459
I slapped together what I think is a better search UI for the new bug tracker, and used that to find it:
Note there's a slight bug in that one needs to clicke the "Search" button instead of just pressing enter (which just errors). I've yet to get around to fixing that.
Just replying to your old comment waaay up the comment stream re why Mark filed the bug in the first place. It was actually after a conversation with me. The specific issue was that I was setting up a Hibernate session and transaction in onRequestStart, and tearing it down again in onRequestEnd (this was before CF ORM). I won't go into the gory details, but basically this is stuff that *must* *MUST* be cleaned up - as in, you need to restart your server if you fail to do so. It seemed to us that onRequestEnd() was there precisely to do this sort of cleanup and that it was just an oversight that it could be bypassed so easily.
As an aside, the CF server itself takes request cleanup very seriously, as all enterprise-grade software should. It *will* cleanup its own dangling transactions, Hibernate sessions etc. So short of putting an axe through the server, the notion that there's any circumstance where a page request just stops abruptly is nonsense. The only thing at issue is whether our third-party code gets to be enterprise-grade as well and provide cast-iron clean-up guarantees.
Anyway, I'm glad to see the concept lived on in the form of onAbort, and didn't evaporate in amongst all the silliness over semantics (Did the request end or didn't it? If it didn't, how did it manage to start twice?) Whatever you think of the various decisions around naming (and in hindsight some weren't the best), the fact is in terms of lifecycle management CF8 was a toy environment and CF9 and 10 are not.
Oh, and I hope Peter and Adam are feeling better and have gotten over wanting to slap the stupid people responsible ;)