ColdFusion in Context: Avoiding Instant Replay

Suppose your user really needs to see new data on every visit to the page. This tip helps you experiment to determine the practical effect of various methods of trying to avoid instant replay in the real world with various browsers and Web servers.

Build a Laboratory

You'll need a laboratory that lets you readily modify header information and whether you can post to the page as well as link to it. Put this code in lab.cfm.

Include a page you'll write to define Greenwich Mean Time (GMT) as an HTTP date/time stamp. Use the ColdFusion createUUID function to provide a universally unique ID string.

Provide code to produce a header for each keyword found in the Mode in the query string. If requested, produce headers containing dates and an entity tag to help browsers and caches decide (if they bother to look) that the content might have changed: a last-modified date, an entity tag, an expiration date, or some combination of these. (By default, the Web server gives the date the page was served; so, you won't have to supply it.)

<cfinclude template="gmt.cfm">
<cfset Unique=createUUID()>

<cfparam name="Mode" default="">
<cfif Mode contains "NewLast">
  <cfheader name="last-modified"
  value="#GMT#">
<cfelseif Mode contains "OldLast">
  <cfheader name="last-modified"
  value="Wed, 08 Jan 2003 05:15:30 GMT">
<cfelseif Mode contains "BadLast">
  <cfheader name="last-modified"
  value="-1">
</cfif>
<cfif Mode contains "NewEtag">
  <cfheader name="etag" value="#Unique#">
<cfelseif Mode contains "OldEtag">
  <cfheader name="etag" value="bogus">
</cfif>
<cfif Mode contains "NewExpires">
  <cfheader name="expires" value="#GMT#">
<cfelseif Mode contains "OldExpires">
  <cfheader name="expires"
  value="Mon, 06 Jan 2003 04:15:30 GMT">
<cfelseif Mode contains "BadExpires">
  <cfheader name="expires"
  value="-1">
</cfif>

Then if requested, add directives to tell the caches and browser how to handle this information: pragma, must-revalidate, no-cache, no-store, private, max-age, or some combination of these.

<cfif Mode contains "Pragma">
  <cfheader name="pragma"
  value="no-cache">
</cfif>
<cfif Mode contains "Must">
  <cfheader name="cache-control"
  value="must-revalidate">
</cfif>
<cfif Mode contains "Nocache">
  <cfheader name="cache-control"
  value="no-cache">
</cfif>
<cfif Mode contains "Nostore">
  <cfheader name="cache-control"
  value="no-store">
</cfif>
<cfif Mode contains "Private">
  <cfheader name="cache-control"
  value="private">
</cfif>
<cfif Mode contains "Maxage">
  <cfheader name="cache-control"
  value="private">
</cfif>

If requested, add a form button to post the file (to see the impact of posting on instant replay).

<cfif Mode contains "Post">
  <form method="post">
  <input type="submit">
  </form>
</cfif>

Finally, display the time and the entity tag. If the user sees the same data twice, instant replay has occurred. Provide instructions for clarity.

<cfoutput>#GMT#<br>
#Unique#</cfoutput>

<p>
Note the time and/or Etag above.<br>
Refresh.  Did the time change?
Use the back button to return.

Get GMT

Put this code in gmt.cfm to build an HTTP date/time stamp for Greenwich Mean Time. It creates a ColdFusion function that uses a Time Zone structure (TZ) to convert local time to universal time. It does this by adding TZ.utcTotalOffset (which might be in minutes as well as hours in some parts of the world) to the current time. It then builds a string containing the date, the time, and the letters "GMT". Because it's easier to display a variable than a function, the code then sets the variable GMT equal to the function for later use.

<cfscript>
function gmt(){
  // Gets GMT date/time (GMT) from local time
  TZ=getTimeZoneInfo();
  DT=now();
  GDT=dateAdd('s',TZ.utcTotalOffset,DT);
  GMTDt=DateFormat(GDT,'ddd, dd mmm yyyy');
  GMTTm=TimeFormat(GDT,'HH:mm:ss');
  GMT="#GMTDt# #GMTTm# GMT";
  return GMT;
}
</cfscript>
<cfset GMT=gmt()>

Make a Menu

To pass a query string into the laboratory in lab.cfm to tell it what to do, create menu.cfm and fill it with code that modifies a link with which you'll call lab.cfm. You'll check options you want included in the string, submit, and then click the link with its modified query string to test the effect of the requested combination of headers.

Provide instructions for six months from now so you 'll know how to use the page. Then to display submitted radio button values, create "Show" variables that are either empty (so the box won't be checked) or the string "checked" to make them checked. Because you need to do this for every possible value of each form variable that's fed by a radio button, the code is more lengthy than you might expect.

Specify headers you want the linked page to use.<br>
The linked page will generate the headers
named in the URL you use to reach it.<br>

<cfset MyMode="">

<cfparam name="form.Etag" default="">
<cfset ShowNoEtag="">
<cfset ShowNewEtag="">
<cfset ShowOldEtag="">
<cfif form.Etag is "">
  <cfset ShowNoEtag="checked">
<cfelseif form.Etag is "NewEtag">
  <cfset ShowNewEtag="checked">
  <cfset MyMode=MyMode&"NewEtag">
<cfelseif form.Etag is "OldEtag">
  <cfset ShowOldEtag="checked">
  <cfset MyMode=MyMode&"OldEtag">
</cfif>

<cfparam name="form.Last" default="">
<cfset ShowNoLast="">
<cfset ShowNewLast="">
<cfset ShowOldLast="">
<cfset ShowBadLast="">
<cfif form.Last is "">
  <cfset ShowNoLast="checked">
<cfelseif form.Last is "NewLast">
  <cfset ShowNewLast="checked">
  <cfset MyMode=MyMode&"NewLast">
<cfelseif form.Last is "OldLast">
  <cfset ShowOldLast="checked">
  <cfset MyMode=MyMode&"OldLast">
<cfelseif form.Last is "BadLast">
  <cfset ShowBadLast="checked">
  <cfset MyMode=MyMode&"BadLast">
</cfif>

<cfparam name="form.Expires" default="">
<cfset ShowNoExpires="">
<cfset ShowNewExpires="">
<cfset ShowOldExpires="">
<cfset ShowBadExpires="">
<cfif form.Expires is "">
  <cfset ShowNoExpires="checked">
<cfelseif form.Expires is "NewExpires">
  <cfset ShowNewExpires="checked">
  <cfset MyMode=MyMode&"NewExpires">
<cfelseif form.Expires is "OldExpires">
  <cfset ShowOldExpires="checked">
  <cfset MyMode=MyMode&"OldExpires">
<cfelseif form.Expires is "BadExpires">
  <cfset ShowBadExpires="checked">
  <cfset MyMode=MyMode&"BadExpires">
</cfif>

To display submitted checkbox values for on-off parameters (well, actually on-no), create "Show" variables that are either empty (so the box won't be checked) or the string "checked" (to check the box).

<cfparam name="form.Must" default="no">
<cfif form.Must is "on">
  <cfset ShowMust="checked">
  <cfset MyMode=MyMode&"Must">
<cfelse>
  <cfset ShowMust="">
</cfif>

<cfparam name="form.Maxage" default="no">
<cfif form.Maxage is "on">
  <cfset ShowMaxage="checked">
  <cfset MyMode=MyMode&"Maxage">
<cfelse>
  <cfset ShowMaxage="">
</cfif>

<cfparam name="form.Private" default="no">
<cfif form.Private is "on">
  <cfset ShowPrivate="checked">
  <cfset MyMode=MyMode&"Private">
<cfelse>
  <cfset ShowPrivate="">
</cfif>

<cfparam name="form.Nostore" default="no">
<cfif form.Nostore is "on">
  <cfset ShowNostore="checked">
  <cfset MyMode=MyMode&"Nostore">
<cfelse>
  <cfset ShowNostore="">
</cfif>

<cfparam name="form.Nocache" default="no">
<cfif form.Nocache is "on">
  <cfset ShowNocache="checked">
  <cfset MyMode=MyMode&"Nocache">
<cfelse>
  <cfset ShowNocache="">
</cfif>

<cfparam name="form.Pragma" default="no">
<cfif form.Pragma is "on">
  <cfset ShowPragma="checked">
  <cfset MyMode=MyMode&"Pragma">
<cfelse>
  <cfset ShowPragma="">
</cfif>

<cfparam name="form.Post" default="no">
<cfif form.Post is "on">
  <cfset ShowPost="checked">
  <cfset MyMode=MyMode&"Post">
<cfelse>
  <cfset ShowPost="">
</cfif>

Now use a cfoutput tag to expand the variables you've defined, and use tables to lay out the labels and controls in a form you can submit to modify a link. The reason for the groupings used here will become clear when you start experimenting.

<cfoutput>

<form method="post">

<table>
<tr><td colspan="4">Entity Tag (Etag):</td></tr>
<tr><td><input type="radio" name="Etag"
value="" #ShowNoEtag#></td>
<td>None</td>
<td><input type="radio" name="Etag"
value="NewEtag" #ShowNewEtag#></td>
<td>New</td>
<td><input type="radio" name="Etag"
value="OldEtag" #ShowOldEtag#></td>
<td colspan="3">Old</td></tr>

<tr><td colspan="4">Last-Modified Date:</td></tr>
<tr><td><input type="radio" name="Last"
value="" #ShowNoLast#></td>
<td>None</td>
<td><input type="radio" name="Last"
value="NewLast" #ShowNewLast#></td>
<td>New</td>
<td><input type="radio" name="Last"
value="OldLast" #ShowOldLast#></td>
<td>Old</td>
<td><input type="radio" name="Last"
value="BadLast" #ShowBadLast#></td><td>Bad</td></tr>

<tr><td colspan="4">Expires Date:</td></tr>
<tr><td><input type="radio" name="Expires"
value="" #ShowNoExpires#></td>
<td>None</td>
<td><input type="radio" name="Expires"
value="NewExpires" #ShowNewExpires#></td>
<td>New</td>
<td><input type="radio" name="Expires"
value="OldExpires" #ShowOldExpires#></td>
<td>Old</td>
<td><input type="radio" name="Expires"
value="BadExpires" #ShowBadExpires#></td><td>Bad</td></tr>

</table>

<table>
<tr><td>Must</td><td><input type="checkbox"
name="Must" #ShowMust#></td>
<td>Maxage</td><td><input type="checkbox"
name="Maxage" #ShowMaxage#></td>
<td>Private</td><td><input type="checkbox"
name="Private" #ShowPrivate#></td></tr>
<tr><td>Nostore</td><td><input type="checkbox"
name="Nostore" #ShowNostore#></td>
<td>Nocache</td><td><input type="checkbox"
name="Nocache" #ShowNocache#></td>
<td>Pragma</td><td><input type="checkbox"
name="Pragma" #ShowPragma#></td></tr>
<tr><td>Post</td><td colspan="5"><input type="checkbox"
name="Post" #ShowPost#></td></tr>
</table>

<input type="submit" name="doit"
value="Submit">
</form>

The link itself is rebuilt every time you submit the form. Provide instructions for the link. Close the cfoutput tag. Provide final instructions relating to the point of the process: to check for instant replay.

AFTER pressing submit and waiting for refresh,
click this link:<br>
<cfif len(trim(MyMode))>
  <a href="ubu.cfm?Mode=#MyMode#">#MyMode#</a>
<cfelse>
  <a href="ubu.cfm">Plain</a>
</cfif>

</cfoutput>

<p>
After returning, click the link again.<br>
Did the time change?

Try Out the Menu

Browse menu.cfm. Select some controls, submit the form, and see the link change. Then 1) try the link, 2) refresh the page it brings you to, 3) use the back button a couple of times to get back to the menu, and 4) try the link again. Notice situations where the time changes and where it doesn't. With many combinations, you'll find that refresh brings back a fresh page but that merely clicking on the link does not.

When "post" is involved, the test procedure is slightly different. Select some controls, including post, and see the link change. Then 1) try the link, 2) press the submit button on the lab page, 3) press the submit button on the lab page again, and 4) refresh the page. Notice the results and circumstances.

Experiment

There are too many possible combinations to conveniently try out, but the following procedure will be useful. Try the date and Etag options to see which ones give you instant replay. Then for each date and Etag option that gives you instant replay, add other options (max age, etc.) to see which ones cure the problem.

Before you begin, review checking and setting browser options.

Internet Explorer's default is to check for newer pages "Automatically". You'll also want to try "Every visit" - the most useful setting - and "Never" - a setting I have often encountered when working with users. From the browser menu, go to Tools..Internet Options..Settings to check and make these settings.

Netscape Navigator's default prior to brain-dead version 6 is to get a fresh page every time. To confirm this, go to Edit..Preferences..Advanced..Cache. Because this setting is usually correct - perhaps company IT folks don't mess with it - it's probably not worth your time to see the result of mal-adjusting Netscape to "Once per Session" or "Never".

Record the Web server you're using. My personal Web server uses HTTP/1.0, the older Web protocol that ignores cache-control directives. My Web host's server uses HTTP/1.1, the newer Web protocol that sometimes obeys them.

Note whether you're using https (secure) or http (the usual); because, this should also (in theory) cause your results to vary.

Finally, if you know that your browser uses a proxy server, note that too. Browse the page from another location (perhaps your home) where the browser isn't set to use a proxy server to see under what circumstances this makes a difference.

To recap, here's the list of things you'll want to take into account. (A spreadsheet is helpful here.)

For each combination you check, record two results. Was the link/post OK? Was the refresh OK?

Results

In the hundreds of trials I made, there were two constants. Changing the URL through a javascript function (so that the link was always unique) prevented instant replay completely. (Changing the URL by having the server rewrite the page (as with ColdFusion), worked as long as the user could not pull the link from history (as with the back button) and re-use it.) Posting the page ALMOST always prevented instant replay.

Problems and their solution varied as shown below.

Conclusion

You want to specify as few headers as possible, and you want to avoid a situation where the only remedy is to post the page.

Recommendation

If you wish to be sure that a page will not receive instant replay, and you don't want to vary the URL or post the page, then do the following: I've confirmed this recommendation against every combination of Web servers, proxy servers, and browsers available to me. Using the tool provided here, you can re-check this recommendation for new browsers and different conditions. Avoid instant replay! =Marty=