Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Asher Snyder
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Asher Snyder

Considering The Aesthetics And Ergonomics Of Post-Back URLs In ColdFusion

By
Published in Comments (10)

Over the years, I've come to believe deeply in the supremacy of the URL. That is, when navigating around a web application, I believe that the vast majority of views should be accessible by URL in order to facilitate deep-linking to anywhere within the app (either in a Single-Page Application context or in a Multi-Page Application context). But, as strongly as I feel about this, I've never quite reconciled it with the way in which I manage my post-back URLs in ColdFusion. As such, I wanted to briefly consider both the aesthetics and ergonomics of post-back URLs.

In this exploration, I'm considering a "post-back URL" to be the URL that goes in the action attribute of a <form> tag. In most of my ColdFusion workflows, forms will submit back to the same page with a submitted=true flag that indicates that the data should be processed (as opposed to initialized). Hence the "post back" nomenclature.

Let's consider a list of widgets that each have an edit link. Each edit link contains the unique id of the target widget:

<h1>
	Widgets
</h1>

<ul>
	<li>
		<a href="./edit.cfm?id=1">Edit - Widget One</a>
	</li>
	<li>
		<a href="./edit.cfm?id=2">Edit - Widget Two</a>
	</li>
	<li>
		<a href="./edit.cfm?id=3">Edit - Widget Three</a>
	</li>
</ul>

All three of these links are GET requests that will take the user to the edit view and load the widget data associated with the given id (note that the data loading is not shown in this demo). Since my ColdFusion workflows all post back to themselves, the edit page will render a form whose action attribute points back to edit.cfm. And, in this incarnation, the id value will appended to the request using a hidden <input> field:

<cfscript>

	param name="url.id" type="numeric";
	param name="form.submitted" type="boolean" default=false;

	if ( form.submitted ) {

		// ... process form submission ....

	}

</cfscript>
<cfoutput>

	<h2>
		Edit Widget
	</h2>

	<form method="post" action="./edit.cfm">
		<input type="hidden" name="id" value="#encodeForHtmlAttribute( url.id )#" />
		<input type="hidden" name="submitted" value="true" />

		Name:
		<input type="text" value="...." />
		<button type="submit">
			Save
		</button>
	</form>

</cfoutput>

This approach has problematic ergonomics regarding the delivery of the id value. On the initial page load, the id is provided in the URL search parameters. However, on post-back (ie, the form submission), the hidden input delivers the id value as part of the form data. Which means that our CFParam tag for url.id will throw an error on post-back:

The required parameter url.id was not provided.

This page uses the cfparam tag to declare the parameter url.id as required for this template. The parameter is not available. Ensure that you have passed or initialized the parameter correctly. To set a default value for the parameter, use the default attribute of the cfparam tag.

Historically, in order to remediate this problem I'll create a unified "context" object that combines both the url and the form scope. Then, I'll use this context object scoping in places where a variable might be provided in either the URL or the form data.

In the following refactoring, notice that I start off by merging the form scope into the url scope (giving the form scope higher precedence). Then, I'll use context.id instead of url.id for all subsequent references:

<cfscript>

	// Create a unified container for URL+FORM variables.
	context = url
		.copy()
		.append( form )
	;

	param name="context.id" type="numeric";
	param name="context.submitted" type="boolean" default=false;

	if ( context.submitted ) {

		// ... process form submission ....

	}

</cfscript>
<cfoutput>

	<h2>
		Edit Widget
	</h2>

	<form method="post" action="./edit-b.cfm">
		<input type="hidden" name="id" value="#encodeForHtmlAttribute( context.id )#" />
		<input type="hidden" name="submitted" value="true" />

		Name:
		<input type="text" value="...." />
		<button type="submit">
			Save
		</button>
	</form>

</cfoutput>

This remediates the CFParam error; but, it opens up a new set of ergonomic and aesthetic problems. First, on page load the URL looks like this:

./edit-b.cfm?id=1

But, upon form submission, the URL looks like this:

./edit-b.cfm

When the form is submitted, the id value has moved from the url scope into the form scope and is no longer needed in the search parameters. This is fine because of the context object; but, it means that the URL no longer represents a valid location within the application routing. Which means, if the user where to focus the location bar and hit Enter, the ColdFusion application would result in an error (since no id value is being provided).

It also means that the URL is no longer shareable after the form has been submitted. Imagine a scenario in which the user is having an issue with the form submission and wants to open a Support Ticket. They might copy-paste the URL into the Support Ticket in an effort to provide more meaningful context; but, all of the meaningful information has been moved out of the URL and into the form data (making their copy-pasted URL meaningless to the Support staff).

To solve both the aesthetic and the ergonomic problems, I realize that what I need to do is keep URL data in the URL and form data in the form. My mistake has always been moving the URL data into the form during the post-back workflow. This has caused me nothing but problems over the years.

Refactoring the example once again, this time I'm going to keep the id in the URL (via the action attribute) and the submitted flag in the form (via the hidden input):

<cfscript>

	param name="url.id" type="numeric";
	param name="form.submitted" type="boolean" default=false;

	if ( form.submitted ) {

		// ... process form submission ....

	}

</cfscript>
<cfoutput>

	<h2>
		Edit Widget
	</h2>

	<form method="post" action="./edit-c.cfm?id=#encodeForUrl( url.id )#">
		<input type="hidden" name="submitted" value="true" />

		Name:
		<input type="text" value="...." />
		<button type="submit">
			Save
		</button>
	</form>

</cfoutput>

This ColdFusion code no longer needs the context object creation since we're propagating the data in the same scopes. The id value stays in the URL, as part of the action attribute, and the submitted value stays in the form. And when the form is submitted, we can see this continued separation in the network activity:

Chrome network activity tab showing the edit-c.cfm page with the id in the URL and the submitted flag in the form data.

Once we decide to keep the URL data in the url scope and the form data in the form scope, we can simplify this even further by using the cgi.query_string variable. This variable simply echoes back what the client provided in the request URL. We can even use the cgi.script_name which echoes back the requested template.

In this final refactoring, we're going to define a postBackAction value at the top of the page and then render it into the action attribute. We only have a single form in this demo; but, we can assume that a variable like this might be defined earlier in the request and then used more widely.

<cfscript>

	// All forms in this ColdFusion application post-back to themselves for processing. As
	// such, we can define a generic post-back action attribute value that any form can
	// use to propagate the request URL.
	postBackAction = "#cgi.script_name#?#encodeForHtmlAttribute( cgi.query_string )#";

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

	param name="url.id" type="numeric";
	param name="form.submitted" type="boolean" default=false;

	if ( form.submitted ) {

		// ... process form submission ....

	}

</cfscript>
<cfoutput>

	<h2>
		Edit Widget
	</h2>

	<!--- Note: Action is using the post-back URL. --->
	<form method="post" action="#postBackAction#">
		<input type="hidden" name="submitted" value="true" />

		Name:
		<input type="text" value="...." />
		<button type="submit">
			Save
		</button>
	</form>

</cfoutput>

With this final approach, we have several benefits:

  • The URL always represents a valid, deep-linkable, shareable location within the ColdFusion application.

  • Data never migrates from one scope to another which means that we never have to combine the url and form scopes into a context pseudo-scope.

  • Form data can never be submitted via the URL as part of a malicious attack since form data will always be accessed via the form scope (which isn't populated during a GET request).

  • We can create a generic postBackAction value in order to make <form> rendering a little less tedious.

  • It helps us remember the supremacy of the URL; and that it should always be meaningful in the current application context.

This is going to be my ColdFusion application strategy moving forward. I can't think of a scenario, off-hand, in which this would be problematic; but, I'll report back if a problem arises.

URL Rewriting Considerations

In the above code, I use the cgi.script_name to generically identify the ColdFusion template being requested. This works fine for basic ColdFusion applications; but, might not work for all types of ColdFusion applications. For example, if an application is using URL rewriting, the "requested template" might be passed to the ColdFusion application as a query string parameter. Or, it might be appended to the resource as the cgi.path_info value. In such cases, it might not be easy to generically define a postBackAction. But, that was just an efficiency, not a necessity.

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

Reader Comments

15,944 Comments

Re: using the cgi.query_string variable; the one thing about that which doesn't sit well with me is that you will end up posting back to a URL that you (as the programmer) don't fully control. Meaning, if a malicious actor crafts a URL to send to someone else that brings that user to a form, the form will end up posting back to a URL that contains whatever information the malicious actor included.

In theory, this shouldn't matter because it shouldn't be any different than the security around any URL (GET or POST). But, you just always want to have it in the back of your mind that you can't trust a URL. As much as you might accept any URL, you have to be sure that you always validate inputs on the server-side before you act on that URL.

15,944 Comments

So as I'm applying this technique to Dig Deep Fitness, I'm realizing that there is a scenario that complicates this a bit: setting default values in a form. In some places, I want to pass a URL-based parameter to the initial form rendering to setup a default; but then, once the form is submitted, I want to use the form-based value. In my current approach, where it's the same variable, keeping things in the URL doesn't make sense.

What I think I'll do, however, is break it up into two values:

  1. The URL-based value sets up the default.
  2. The FORM-based value is what gets processed.

I can handle this by using two different cfparam tags, like:

// The URL-based value.
param name="url.defaultThing" default="";

// ... which can be used to default the FORM-based value.
param name="form.thing" default=url.defaultThing;

Notice that it's two different, non-colliding names (url.defaultThing and form.thing). Once the form submission starts processing, the url value essentially gets discarded and the form value is what is consume.

15,944 Comments

Ok, one more caveat I've run into while trying to apply this approach. My initial mechanics are to use the cgi.query_string. But, I do have some control-flows where I redirect the user to a new page with some transient information in the URL (specifically a "success message" flag so that the app renders a message to the user). If this new page also contains a form that posts back to itself, the cgi.query_string will contain that transient information. Of course, I don't actually want that transient information to be encoded into the action attribute of the form.

So, I can do one of two things:

  1. I can use some pattern matching to replace-out the transient data in the postBackAction variable at the time I define it.

  2. I can manually construct the postBackAction from the url scope, explicitly skipping over some block-listed keys.

I think either approach is completely valid. I'll likely start with the regular expression replace initially and see how it goes. But, the more keys that might need to be omitted, the more it might make sense to manually construct the variable string.

And, it's also important (for me) to keep in mind that the postBackAction is intended to be an efficiency, not a mandate. If it proves to be more trouble than it's worth, I can just explicitly define the search parameters in the action attribute directly.

6 Comments

Hey Ben - we have survived Christmas, mostly. I wanted to share some elaborate thoughts on this as I use a bit of a different strategy. I may post about it just to help with flow as posting a reply might be difficult to follow.

I've embraced a bit of the old fusebox method by always providing a hidden fuseaction field in my form submits. e.g. fuseaction=post-article-reply and in your example above, an additional hidden field - for aticleid = 1.

On all form submits - I have a custom tag that changes all form and URL elements to variables. So, managing the request on a page doesn't really know where it comes from.

All request end up going to an index page that has a command_layer.cfm which handles what happens based on the fuseaction. In this case - it runs into a cfswitch with the "post-article-reply". Executes the post and additional code can retrieve the article to display it.

As I continue to type, I am realizing how difficult the response is to follow. But here's a short diagram(ish) of the structure.

Post to index.cfm of the current folder...

--- cfinclude variables.cfm (file that accepts and creates defaults)
--- cfinclude command_layer.cfm (file that runs commands based on fuseaction) this file has cfinvokes to a queryprocessing cfc module
--- cfinclude presentation_layer (this file also has switches and displays the page to render based on fuseaction

I think I'll use the rest of my weekend to get my blog up!!! Best wishes fellow developers. Hope everyone had a great holiday season.

15,944 Comments

@Angel,

Merry Christmas to everyone's favorite "Chicken Dad" 😆 It actually sounds like our various approaches are fairly similar. When I'm not using FW/1 at work, I too am using what amounts to a fusebox-esque approach to architecture. I have nested switch statements which route the request to the correct controller file; and then I roll each rendered view up into a page. It sounds like we just maybe organize it a little differently, but the high-level idea sounds very familiar to me.

If you do write something down this weekend, please cross-post it here so we can take a look 🙌

455 Comments

Hi Ben

I think this is the approach that Coldbox [controller rc scope] & FW1 [controller rc scope].
In Coldbox, the prc scope stands for private request context scope, which refers to variables created inside a controller method, like the local scope. The rc scope is a pass through scope, generally originating from a form or url variable.
Interestingly, in non framework Coldfusion applications, I often use the Post/Redirect/Get (PRG) Design Pattern:

https://www.geeksforgeeks.org/post-redirect-get-prg-design-pattern/

But with a twist.

Before I do the cflocation redirect, I set a session variable that will allow me to do stuff, after I get redirected. In fact, most of the time, this isn't even required.

The PRG pattern prevents those annoying Do you wish to resubmit this form? message.

15,944 Comments

@Charles,

Ah, very nice, I didn't realize this pattern had a name. But, totally, this is the way to do things as far as I'm concerned. Even when I need re-render the same page, I'll always do a redirect back to the same page to avoid the form submission problem.

As far as setting the post-submission session stuff, I go back and forth on different ways to do this kind of thing. The approach that I've been playing with recently is appending 1 or 2 variables in the URL that indicate a "success" condition. Like:

/index.cfm?flash=user.created&flashData=123

Then, I have a centralized point in my controller-flow that will examine the flash / flashData and render a success message and typically perform a history.replaceState() call on the client-side to strip the flash / flashData out of the URL so that if the user refreshes the page, the success message will disappear.

It's not a perfect solution - I never quite love any of the solutions I come up with. But, I'm liking it for the moment.

6 Comments

@Ben Nadel, @Charles Robertson

Just a geeky thought - I shy away from using the redirects. To me - those things are the death of debugging if not done with good foresight. I have pretty much taken them out of every project I have worked with.

455 Comments

@Angel Gonzalez,

I am kind of talking about cflocation, using the PRG pattern? I think Coldbox & FW1 encourage the use of redirect() at the end of controller methods, which emulates PRG.
I guess, if this is used at the end of a call chain, then hopefully there shouldn't be a debugging issue. 🙂

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

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel