Skip to main content
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Ray Camden
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Ray Camden ( @cfjedimaster )

Custom CFParam Tag That Exposes Error Information In Lucee CFML 5.3.7.48

By on
Tags:

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.

Want to use code from this post? Check out the license.

Reader Comments

4 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.

15,334 Comments

@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!

Post A Comment — I'd Love To Hear From You!

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.