Wednesday 5 December 2012

I love being wrong

That's not sarcasm, I actually do rather like being wrong about stuff: it's a great vector for learning new things and improving one's knowledge. Provided one doesn't get defensive about being wrong (which if just daft), and does endeavour to understand how one was wrong, and do something about it.

Anyway, enough airy-fairy philosophising. What was I wrong about?

The form scope.

Yeah, not something esoteric or complicated.

This all arise from my favourite place of contention: Stack Overflow. Someone was having problems with variables sticking in the session scope: they were vanishing between requests. The convolution here was that the variables originated from the form scope. Well it was the entire form scope. The detail is on the Stack Overflow page: "Application.cfm missing Session variables after 301 redirect" (the title is not accurate as to what the issue actually is, but it's certainly a symptom of it), but in summary they needed to "persist" the form scope across a redirect, so they were popping it into the session scope temporarily, and extracting it again on the other side. Fairly straight-forward stuff. Except when they landed on the other side of the redirect, the variable in the session scope holding the form scope was empty. It was there, but was empty.

Here's some simplified code that demonstrates this:

<!--- Application.cfm --->
<cfapplication name="test" sessionmanagement="true">

<!--- form.cfm --->
<form method="post" action="./action.cfm">
        <tr><td>Favourite Number:</td><td><input type="text" name="favouriteNumber" value="" /></td></tr>
        <tr><td>Favourite Colour:</td><td><input type="text" name="favouriteColour" value="" /></td></tr>
        <tr><td>Favourite Place Name:</td><td><input type="text" name="favouritePlaceName" value="" /></td></tr>
        <tr><td colspan="2" valign="right"><input type="submit" name="btnSubmit" value="Submit" /></td></tr>

<!--- action.cfm --->
<cfdump var="#form#" label="form">

<cfset session.postData = form>

<cfdump var="#session#" label="session">

<!--- check.cfm --->
<cfdump var="#session#">

What's going on here is that the form is submitted, a reference to the form scope is placed in the session scope. Simple. I've pared this down as much as possible, and even removed the redirect in case that was messing with things. So basically we submit the form, action it, then via a separate request inspect what's in the session scope. And that would be: Bugger-all. Here's the output from the code.


form - struct
FAVOURITENUMBERwha tekau ma rua
session - struct
session - struct
FAVOURITENUMBERwha tekau ma rua

(and, yes, that is an actual place name. Look-it up).

And the output from check.cfm:

struct [empty]


As the output demonstrated, the form variables were all there in the form scope, and indeed were also there in the session scope. On the next request, the same session scope was there (ie: it's not that the session isn't being maintained), and the reference to the form scope is there, but no variables.

Here's the bit where I was wrong. And emphatically wrong. And not only that, I was "correcting" sometime who was actually right. I hate it when that happens to me (and I react poorly to it), so I apologise to Travis for correcting him when he was actually right, as well as unnecessarily prolonging the Stack Overflow thread. Not my finest hour.

Travis had said this:

Once your form structure no longer contained data your postData structure began referencing an empty structure so your reference is also empty.

And I "corrected" him with this:
What @Travis said was the problem - that "when the form structure no longer exists, neither does your postdata structure" - is absolutely not true. The reference to the form scope might have gone, but as long as there's any reference to the data (like request.session.postData) then the underlying object will not be removed.

Which all sounds good superficially, but is not correct.

I looked into what's going on, and indeed when the form scope gets killed at the end if a request, it's actually first cleared, then it's deleted. I just ass-u-me`d it was simply deleted, but as there was still another reference to the underlying data, the data would not be cleared up, it would stick around.

Basically I was assuming this sort of thing was happening (CFML pseudocode for clarity):

structDelete(someStruct, "form");

But what is actually kinda happening is this:

structDelete(someStruct, "form");

(obviously it's done in Java, so it'd be more like this):

form.clearItAllOut(); // probably actually clear() or clearVariables(), looking at the methods the form scope implements
form = null;

So this not only deletes the form scope reference (which should have been sufficient), it first goes and blitzes all the data.  Which kinda screws things up if one's still referencing that data!

So thanks for that, ColdFusion.

Incidentally, I tried to run this code on Railo (I've not upgraded to 4.0.2 yet), but I just got a stack overflow exception when dumping the session scope - or referencing anything to do with it, actually - so I can't say what Railo does to the form scope. I will be looking more into this later.

Anyway, the bottom line for me here is that I think ColdFusion is being unhelpful when clearing the form scope. Just deleting the reference should be fine: GC will take care of the underlying data if there are no more references to it, just like with any object.

Travis said one other thing I still need to follow up, but it's after 9am now, so I need to do some work. Oh, and sorry if there are any residual "autocorrect dodginess" in this article: I wrote most of it on my phone on the train en route to the office. I think I found them all, but I always miss some.