Setting ETag Headers Regardless Of File Content In ColdFusion
Last week, at InVision, while trying to improve the overall experience of one of our application pages, I started looking into ETag header values to decrease page load times. The first post that I came across (or rather, that I came across "again") was this one by David Boyer. In it, David outlines what ETag headers do and how to apply them in ColdFusion. Only, in my case, I didn't want the value of the ETag header to be based on the file content - I wanted it to be based on the state of the application.
In my applications, I tend to store an "application version" number. This version number is simply a numeric representation of the date/time on which the application was last initialized. This turns out to be a super useful property when it comes to problems like managing caching and keeping sessions in sync with the application.
Application.cfc - Our ColdFusion Application Framework
<cfcomponent
output="false"
hint="I define the application settings and event handlers.">
<!--- Define the application settings. --->
<cfset this.name = hash( getCurrentTemplatePath() ) />
<cfset this.applicationTimeout = createTimeSpan( 0, 0, 5, 0 ) />
<cfset this.sessionManagement = false />
<!--- Turn of debugging. --->
<cfsetting showdebugoutput="false" />
<cffunction
name="onApplicationStart"
access="public"
returntype="boolean"
output="false"
hint="I initialize the application.">
<!---
Store a version number for the application - a numeric
version of the date on which it was initialized.
--->
<cfset application.versionNumber = (now() * 1) />
<!--- Return true so the application can finish loading. --->
<cfreturn true />
</cffunction>
<cffunction
name="onRequestStart"
access="public"
returntype="boolean"
output="false"
hint="I initialize the request.">
<!--- Check to see if we need to manually reset the app. --->
<cfif structKeyExists( url, "init" )>
<cfset this.onApplicationStart() />
</cfif>
<!--- Return true so the request can finish loading. --->
<cfreturn true />
</cffunction>
</cfcomponent>
As you can see, this "versionNumber" property simply gets set every time the onApplicationStart() event handler runs, whether implicitly executed by the ColdFusion Application Framework, or explicitly executed by your request logic.
Since I wanted to define an ETag header for a file, regardless of the content, I figured that I could use a combination of the file path and this application version number. Together, these two values would provide a unique yet controllable ETag input.
To test this, I set up a simple page that adds an ETag header to a response that outputs the current time.
Frame.cfm - Our ETag-Enabled ColdFusion Page
<!---
Define the ETag hash based on the file and the current version of
the application. In this way, we don't have to capture the output
of the page.
--->
<cfset etag = hash( getCurrentTemplatePath() & application.versionNumber ) />
<!---
Check to see if the browser is checking for version updates.
NOTE: The ColdFusion CGI object will return the empty string for
non-existent key requests. As such, we don't have to check for
the browser to pass it - we only have to check the present value.
--->
<cfif (cgi.http_if_none_match eq etag)>
<!--- Tell the browser that the content is the same. --->
<cfheader
statuscode="304"
statustext="Not Modified"
/>
<!---
Don't bother processing - the browser will used the cached
version on the local machine.
--->
<cfexit />
</cfif>
<!---
If we made it this far, the client either doesn't support ETags
or it no longer has the file cached locally. In either case,
let's provide the ETag header so that browser can try to use it.
--->
<cfheader
name="etag"
value="#etag#"
/>
<!---
NOTE: You would probably *also* provide Expires header
information so that the browser could cache the page without
making a request to check for updates; that is beyond the point
of this post, however.
--->
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<cfoutput>
<!doctype html>
<html>
<body>
<p>
The time is now: #timeFormat( now(), "HH:mm:ss.l" )#
</p>
</body>
</html>
</cfoutput>
As you can see, the ETag hash is powered by the file path and the application version number. This hash value is then compared against the "If-None-Match" header provided by the browser. If these two values match, the request is simply terminated with a "Not Modified" response. In this approach, not only do we detach the ETag value from the file content, we remove the overhead of processing the request in order to determine the ETag response.
To pull this together, I created an index file that loads the above ColdFusion page as an IFrame:
<!doctype html>
<html>
<head>
<title>Programmatically Setting ETags In ColdFusion</title>
</head>
<body>
<h1>
Programmatically Setting ETags In ColdFusion
</h1>
<iframe
src="./frame.cfm"
width="500"
height="100">
</iframe>
<p>
<a href="./index.cfm">Refresh page</a>
</p>
<p>
<a href="./index.cfm?init=true">Refresh page with Init</a>
</p>
</body>
</html>
As you can see, in addition to the IFrame, it has two test links: one to refresh the page; and, another to refresh the page while re-initializing the application (ie. defining the "init" query string). And, as you can see in the network activity below, subsequent page loads show a cached version of the frame:
I definitely understand basing ETag headers on file content; however, in my applications, we tend to use a re-initialization event to bust caches. As such, using the application version number in combination with the path of a given file provides an easy way for me to use ETag headers with little overhead.
As a final note, I should mention that I am also adding "Expires" headers to my response. I don't do that in this demo (so that I can see the HTTP requests being made); however, in production, I would also add an Expires header to minimize the number of HTTP requests that need to be made.
Want to use code from this post? Check out the license.
Reader Comments
Hey Ben, thanks for the wonderful tutorial. I haven't been able to get this to work in safari. For some reason safari is not sending an http_if_none_match in the request header for my cfm. Can you confirm this or am I just going crazy?
Hey Ben, got a quick question for you. I have added a cache manifest file for HTML5 coding and made use of the cfcache tag as such; placed above the body tag.
When placing the cfcache with the function use Cache is true and with a trailing slash or a closing /cfcache tag wrapped around my content to cache; it produces duplication errors with my cfform, masks and cfhtml tag. I have been discussing this one with Ray too. I believe it to be a CF10 bug.
But with no useCache is true, no trailing slash or /cfcache closing tag it no-longer duplicates the output of those tags and files.
I was also wondering whether the meta cache-controls are needed if the cfheader tags are being used or if the HTML5 manifest attribute is being used on the pages or if the cfcache is needed at all?
The server is caching the pages and when adding the manifest file with meta tag's the browser is caching the files needing to be cached. Test it...