The "Top" Argument In Dump() Will Not Protect You From Circular References In Lucee CFML 5.3.3.62
The other day, I had a typo in my ColdFusion code that was accidentally creating a circular reference in one of my data-structures. The workflow that was consuming this data-structure dealt with serialization; and, attempting to serialize the accidental circular reference was completely locking-up my Docker container. To debug this, I tried to use the dump()
function with the top
argument in an effort to see where in the structure the problem was residing - something that I demonstrated in Adobe ColdFusion 14-years ago. However, it turns out that the top
argument won't actually protect you form a circular reference in Lucee. As such, I wanted to see if I could build a wrapper to the dump()
built-in function (BIF) that would safely handle circular references in Lucee CFML 5.3.3.62.
To give you some more context, the problematic ColdFusion code looked like this:
<cfscript>
var values = [];
otherValues.each(
( value ) => {
// .... logic ....
// NOTE: TYPO - value(s) was supposed to be value.
values.append( values );
}
);
// .... logic ....
serializeJson( values );
</cfscript>
As you can see, I had accidentally appended the variable values
(with an s
) when I had meant to append the variable, value
(with no s
). What this did was append the Array to itself over-and-over again, creating a bevy of circular references. And, when I then called serializeJson()
, my Docker container would completely lock-up and I would have to force-quite out of Docker For Mac.
I knew something was wrong with the data-structure; but, I didn't know what it was. So, I attempted to use the top
argument for dump()
in an effort to incrementally output the values
object, looking for suspicious data:
<cfscript>
// .... logic ....
dump( var = values, top = 3 );
</cfscript>
But, when I used dump()
, my Docker container would lock-up; and, again, I'd have to quite out of my Docker For Mac.
This morning, I tried to replicate the same conditions in my CommandBox. And, at least CommandBox doesn't lock-up like my Docker container did. Instead, Lucee CFML gives me a reasonable error:

Here, we can see that Lucee is running into a StackOverflow error when trying to call .hashCode()
on some value internally. Not knowing much about Java, my guess is that the .hashCode()
of a ColdFusion Array (List) or a Struct (HashMap) is calculated by aggregating the .hashCode()
calls of its member values. This would lead to infinite recursion given a circular reference.
To get around this problem, I wanted to create a wrapper to the dump()
function that would handle circular references a bit more gracefully. And, I clearly had to do this in such a way that didn't require calling any .hashCode()
methods.
Luckily, I discovered that the triple equals operator (===
) in Lucee CFML will compare object references for complex objects. This gives us a way to see if two variables reference the same physical value.
To leverage this feature, I can keep an Array of complex objects. And then, given a value, I can brute-force loop over that Array and compare the given value to each existing value in the Array using ===
.
ASIDE: I couldn't use
array.contains( value )
as this function uses the.hashCode()
under the hood (as I found out) and creates the same infinite recursion that we're trying to avoid.
Ultimately, my solution was to recursively traverse a given data-structure, creating a deep copy of it that would replace circular references with the string, [circular reference]
. And then, pass this deep-copy off to the native dump()
function so that Lucee could work its normal magic.
I called this function dumpSafely()
:
<cfscript>
/**
* I wrap the execution of dump(), replacing circular references with the string,
* "[circular reference]". This works by recursing down through the data structure and
* keeping track of Complex Objects. This is much slower than the native dump(); but,
* at the cost of being somewhat safer for debugging.
*
* @var I am the value being dumped.
*/
public void function dumpSafely( any var ) {
var complexObjects = [];
// I check to see if the given Complex Value has been seen before. All complex
// objects are kept in an Array; then, when checking, we brute-force loop over
// the array and see if any of the object references match.
// --
// NOTE: We CANNOT USE complexObjects.contains() as that would cause the same
// stack-overflow problem that dump() runs into with calls to .hashCode().
var hasBeenSeen = ( value ) => {
for ( var seenObject in complexObjects ) {
if ( seenObject === value ) {
return( true );
}
}
complexObjects.append( value );
return( false );
};
// I return a copy of the given value that can be safely passed to dump().
var safeCopyForDumping = ( value ) => {
if ( isNull( value ) ) {
return;
}
if ( isSimpleValue( value ) ) {
return( value );
}
if ( ( isStruct( value ) || isArray( value ) ) && hasBeenSeen( value ) ) {
return( "[circular reference]" );
}
if ( isStruct( value ) ) {
// CAUTION: ColdFusion Components pass the isStruct() decision function,
// but do not have a .map() function. As such, we are using the safer
// built-in function, structMap().
return structMap(
value,
( key, subvalue ) => {
return( safeCopyForDumping( subvalue ?: nullValue() ) );
}
);
}
if ( isArray( value ) ) {
return arrayMap(
value,
( subvalue, index ) => {
return( safeCopyForDumping( subvalue ?: nullValue() ) );
}
);
}
// If we're not explicitly testing for a given value type, just pass-through
// the given value as-is.
// --
// NOTE: The Query / ResultSet data-type seems to already handle circular
// references property, using a "Reference" ID in lieu of the circular
// reference.
return( value );
}; // END: safeCopyForDumping.
arguments.label = ( arguments.keyExists( "label" ) )
? "DUMP SAFELY ( #arguments.label# )"
: "DUMP SAFELY"
;
arguments.var = safeCopyForDumping( arguments.var ?: nullValue() );
// Now that we've replaced the "var" argument with one that is safe for dumping,
// we can go ahead and call the native dump() method with all additional
// arguments that may have been passed-in.
dump( argumentCollection = arguments );
}
</cfscript>
As you can see, the dumpSafely()
function keeps a running aggregate of Arrays and Structs in the variable, complexObjects
. Then, every time my deep-cloning algorithm runs into an Array or a Struct, it first checks to see if the value exists in the complexObjects
collection. And, if it does, it replaces the reference with the string, [circular reference]
.
Once the deep-clone has been made, I just pass it off to dump()
, along with any other arguments that were originally passed into the dumpSafely()
function.
ASIDE: Interestingly enough, the Query / RecordSet type already seems to handle circular references gracefully, replacing them with "Reference XXX".
To see this in action, let's create a Struct with some wonky circular reference action:
<cfscript>
values = {
a: {
b: {
c: {
n: nullValue()
},
cd: 3
}
},
aa: {
bb: "bbthing",
cc: "ccthing"
},
aaa: {
thing: new Thing()
},
aaaa: [
"blooper",
"moopsy"
]
};
// Create circular references in the data-structure.
values.a.b.c.values = values;
values.aaa.circ = values;
values.aaa.thing.oops = values.aaa.thing;
values.aaa.thing.doops = values;
values.aaaa.append( values.aaa.thing );
// dump( var = values, top = 2 );
include "./dump-safely.cfm";
dumpSafely(
var = values,
label = "Testing Circular References"
);
</cfscript>
As you can see, I'm creating a cornucopia of circular references. And, when we pass this data off to dumpSafely()
, we get the following output:

Each of the circular references that I created in my data-structure was gracefully replaced with, [circular reference]
, inside of my deep-copy. This allows the underlying call to dump()
to execute without infinite recursion!
Obviously, my dumpSafely()
wrapper is going to take a performance hit by both making a deep-copy of the given variable and then having to iterate over an internal array, checking every complex data-type; and, I'm going to lose the fidelity of some of the data-types (namely ColdFusion components which now get reported as Structs); but, seeing as this is a hail-Mary approach to debugging the circular references in my code, it seems reasonable enough to me in Lucee CFML 5.3.3.62.
Reader Comments
Post A Comment