Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Custom CFParam Tag That Exposes Error Information In Lucee CFML 5.3.7.48

By Ben Nadel on
Tags: ColdFusion

Over on the Lucee CFML developer forum, there was a discussion about accessing cfparam error information in the event that a type-casting error occurs. Currently, you would have to do some fancy string-parsing to get this data out of the given error object. Since I've been heads-down in ColdFusion custom tags lately, I thought it would be a fun code kata to create a ColdFusion custom tag that wrapped the cfparam tag and then exposed exactly this type of information in the event of a parameterization error in Lucee CFML 5.3.7.48.

ASIDE: This is not the first time I've considered wrapping the cfparam tag. In fact, I first tried this 14 years ago (in 2007) when exploring the idea of adding a catch attribute to the cfparam tag.

When you throw an error object in ColdFusion, all of the error object attributes are strings. Unless you create a custom Java Exception object, which feels like a heavy-lift for this exploration. As such, the error details that I provide in this code kata are going to be serialized JSON (JavaScript Object Notation) values stuffed into the detail and extendedInfo string attributes.

  • detail - I will contain the serialized error object thrown by the underlying cfparam tag.

  • extendedInfo - I will contain the serialized error information that provides clearer insights as to why the aforementioned error was thrown.

Essentially, my ColdFusion custom tag is not much more than a glorified try/catch block that wraps the native cfparam tag. I'm calling this ColdFusion custom tag, specify.cfm, because naming stuff is hard:

<cfscript>

	param name="attributes.name" type="string";
	param name="attributes.type" type="string";
	// NOTE: The "default" attribute is OPTIONAL.

	try {

		// Translate parameter reference into a CALLER-scoped reference so that we will
		// check and mutate the calling context, not the current ColdFusion custom tag
		// context.
		callerReference = "caller.#attributes.name#";

		param
			name = callerReference
			type = attributes.type
			default = ( attributes.default ?: nullValue() )
		;

	} catch ( any error ) {

		// The Detail attribute will contain the underlying error object, with the
		// "caller." stripped-out. This should help it read as though the error happened
		// in the calling context, not in the ColdFusion custom tag context.
		detailString = serializeJson( error )
			.replace( "caller.", "", "all" )
		;

		// The ExtendedInfo attribute will contain details about the parameter that is
		// being specified, including whether or not the reference was already defined.
		extendedInfo = attributes.copy();
		extendedInfo.isDefined = isDefined( callerReference );
		extendedInfo.definedValue = ( extendedInfo.isDefined )
			? getVariable( callerReference )
			: nullValue()
		;
		extendedInfo.hasDefault = attributes.keyExists( "default" );
		extendedInfoString = serializeJson( extendedInfo );

		throw(
			type = "SpecifyFailure",
			message = "Underlying cfparam failed within cf_specify.",
			detail = detailString,
			extendedInfo = extendedInfoString
		);

	}

	exit method = "exitTag";

</cfscript>

As you can see, this ColdFusion custom tag is really just a glorified try/catch block that takes the given error information and exposes it in a more "structured" way in the subsequently-throw error.

Now, we can consume cf_specify in essentially the same way that we would have consumed the cfparam tag:

<cfscript>

	// Explicitly build up the struct.
	cf_specify( name = "url.foo", type = "struct", default = {} );
	cf_specify( name = "url.foo.bar", type = "struct", default = {} );
	cf_specify( name = "url.foo.bar.baz", type = "string", default = "Explicit struct creation!" );

	// Implicitly build up the struct (ie, have ColdFusion create the parent structs as
	// needed when param'ing the variable).
	cf_specify( name = "url.hello.world", type = "string", default = "Implicit struct creation!" );

	// Try with some array values.
	url.values = [];
	cf_specify( name = "url.values[ 1 ]", type = "string", default = "Array value 1 creation!" );
	cf_specify( name = "url.values[ 2 ]", type = "string", default = "Array value 2 creation!" );
	cf_specify( name = "url.values[ 3 ]", type = "string", default = "Array value 3 creation!" );

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	// Make sure the default-less calls work.
	cf_specify( name = "url.foo.bar", type = "struct" );
	cf_specify( name = "url.hello.world", type = "string" );

	try {

		// This one should fail (can't cast String to Boolean):
		cf_specify( name = "url.values[ 2 ]", type = "boolean" );

		// ... as such, we should never hit this throw.
		throw( type = "WeShouldNeverGetThisFar" );

	} catch ( "WeShouldNeverGetThisFar" error ) {

		dump( error );
		abort;

	} catch ( any error ) {

		// Swallow error in this example.

	}

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	try {

		// CAUTION: We know the "baz" value is a string and the TYPE here will fail.
		cf_specify( name = "url.foo.bar.baz", type = "boolean" );

	} catch ( "SpecifyFailure" error ) {

		// The SpecifyFailure error contains JSON-serialized information about the root
		// error, including what was asked and what was provided.

		// Underlying CFParam error:
		dump(
			label = "Underlying CFParam error",
			var = deserializeJson( cfcatch.detail ),
			show = "type, message, detail"
		);

		// What was provided:
		dump(
			label = "CF_Specify information",
			var = deserializeJson( cfcatch.extendedInfo )
		);

	}

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	dump(
		label = "URL Scope",
		var = url
	);

</cfscript>

The top portion of this ColdFusion file is just exercises the cf_specify tag; but, the bottom portion is where the magic happens - notice that in the catch block of our error, I am deserializing and outputting the .detail and .extendedInfo properties of the caught error object. And, doing so gives us the following page output:

The underlying error information for the cfparam tag in Lucee CFML.

As you can see, we can easily access the following:

  • The value we were param'ing already existed.
  • The value we were param'ing had the existing value, Explicit struct creation!.
  • The value we were param'ing was a string.
  • The value could not be cast to a boolean.

Given this clear error information, we can implement error handling techniques with better precision. For example, we could catch "hacking attempts" and block IP addresses. This type of error:

Can't cast String [j_security_check] to a value of type [numeric]

Could now be easily checked because I would quickly see two pieces of information:

  • extendedInfo.type : numeric
  • extendedInfo.definedValue : j_security_check

And, I could have a large switch statement at the root of my error-handling that looks for type-casting errors with defined values and then blocks IP-addresses based on a known list of offenders like:

  • j_security_check
  • passwords.txt
  • file:
  • bxss
  • nslookup
  • @@
  • -1 OR
  • config.ini

Of course, you don't want to go too crazy or you might start blocking legitimate bugs in your code (that need to be fixed, not blocked).

Anyway, just a fun little ColdFusion code kata on a quiet Sunday morning.



Reader Comments

Awesome work. I ended up doing something similar to handle my cfparams better. I haven't rolled it out everywhere yet, but the spots where I have, it's been working a treat.

One thing you might need to add to make this work more broadly though is support for the pattern, min, max, maxlength attributes that cfparam also has.

Reply to this Comment

@Peter,

That's awesome to hear that 1) this is on the right track and 2) it seems to work in a real world scenario. Thanks for the feedback!

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.