Using A Tracer Cookie To Watch For Browser Download-Prompts In Lucee CFML 5.3.6.61
Yesterday, I was working on some report-generation for InVision that used Content-Disposition: attachment
in order to prompt the user to save the generated content. My current approach is just to initiate the download in a new tab, which the browser automatically closes once the report has been generated. This approach is fine; but, it got me wondering if I could hook into the life-cycle of the report-generation and download-prompt programmatically. To do this, I wanted to explore the use of cookies in Lucee CFML 5.3.6.61.
First Attempt: Using an IFrame
My first attempt at hooking into the download-prompt was to try and route the report-generation through an iframe
. The hope was that I could listen for the load
event on the iframe
itself, which would tell me when the user was prompted for the download.
Unfortunately, it doesn't appear that the browser emits a load
event on iframe
elements that don't render content.
Second Attempt: Nested IFrames
My second attempt was to nest one iframe
element within another and then generate the report within the nested iframe
. The hope here was that the outer iframe
would have a load
event since it generated content; but, that its load
event would be tied to the loading of the inner iframe
which was triggering the download.
This actually worked in modern browser (Chrome, Firefox, Safari). However, it did not work in IE11. And, since InVision still supports IE11, I had to find a solution that was more broadly supported.
setInterval()
Third Attempt: Tracer Cookie and My third attempt is based on an old jQuery plug-in by John Culviner. The idea behind this approach is that the report-generation code returns, as part of its HTTP Response, a Set-Cookie
header that contains a unique tracer token. The client-side code can then start polling the document.cookie
value for said unique token; and, when it shows up, we can be confident that the report-generation has been completed (and that the user has been prompted to download the binary).
To explore this idea, I created a ColdFusion end-point that uses the sleep()
function in order to simulate report-generation processing time. This CFML page accepts a reportID
parameter; and, if present, echoes it using a CFCookie
tag:
<cfscript>
param name="url.delay" type="numeric";
param name="url.reportID" type="string" default="";
// SIMULATE PROCESSING TIME WITH SLEEP COMMAND!
sleep( url.delay );
// SIMULATE PROCESSING TIME WITH SLEEP COMMAND!
reportName = "Report-#createUniqueID()#.txt";
reportContent = "Your report was generated in #numberFormat( url.delay )#ms";
// If the calling context is looking for a report-generation cookie, let's set one
// that will expire in a few minutes. Even though the browser will be prompted to
// download the report, the resulting "Set-Cookie" header will still get processed
// by the browser.
if ( url.reportID.reFind( "^report_\d+$" ) ) {
cookie
name = url.reportID
value = ""
expires = getHttpTimeString( now().add( "n", 1 ) )
preserveCase = true
;
}
header
name = "content-disposition"
value = "attachment; filename=""#reportName#""; filename*=UTF-8''#encodeForUrl( reportName )#"
;
content
type = "text/plain; charset=utf-8"
variable = charsetDecode( reportContent, "utf-8" )
;
</cfscript>
As you can see, if the url.reportID
parameter is present, we set a cookie using the same name that expires in a minute. We don't need the cookie to last for very long since we're only checking for its existence and then discarding it.
Now that we have this ColdFusion code in place for our simulation, let's look at how we can use the reportID
cookie to monitor for the download-prompt. In the following code, I'm going to intercept the click
event on the report-generation links and then augment the URLs with unique reportID
parameters. This will change the href
values without disrupting the browser's native behavior.
Once the browser redirects to the augmented href
location, I render an overlay that educates the user about possible load-times. Then, I start polling the document.cookie
payload, looking for the report token; and, if it shows up - indicating that the user has been prompted - I hide the overlay and kill the timer.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>
Using A Cookie To Watch For Browser Download-Prompts In Lucee CFML 5.3.6.61
</title>
<link rel="stylesheet" type="text/css" href="./demo.css" />
</head>
<body>
<h1>
Using A Cookie To Watch For Browser Download-Prompts In Lucee CFML 5.3.6.61
</h1>
<h2>
Reports
</h2>
<p>
This report does some light processing (<strong>1,000 ms</strong>).
<a href="./report.cfm?delay=1000" class="report">Run report</a> »
</p>
<p>
This report does some medium processing (<strong>3,000 ms</strong>).
<a href="./report.cfm?delay=3000" class="report">Run report</a> »
</p>
<p>
This report does some heavy processing (<strong>5,000 ms</strong>).
<a href="./report.cfm?delay=5000" class="report">Run report</a> »
</p>
<div class="overlay">
<h3>
Your Report Is Being Generated
</h3>
<p>
Depending on the report, it may take a few moments.
</p>
</div>
<!-- --------------------------------------------------------------------------- --->
<!-- --------------------------------------------------------------------------- --->
<script type="text/javascript">
document.addEventListener( "click", handleClick );
// I watch for click events on the document in an effort to intercept Report
// requests that will prompt the user for a download.
function handleClick( event ) {
if ( event.target.classList.contains( "report" ) ) {
watchForReportPrompt( injectReportID( event.target ) );
}
}
// I inject a reportID into the target HREF and return the ID.
function injectReportID( target ) {
var reportID = ( "report_" + Date.now() );
// Get the current HREF, strip-out any existing report ID from a
// previous report request, and inject the new report ID.
var href = target
.getAttribute( "href" )
.replace( /&reportID=report_\d+/i, "" )
.concat( "&reportID=" + reportID )
;
target.setAttribute( "href", href );
return( reportID );
}
// I show the "report is generating" overlay. Then, hide the overlay when the
// reportID cookies has been detected; or, if a max timeout has been reached.
function watchForReportPrompt( reportID ) {
// Show the overlay.
var overlay = document.querySelector( ".overlay" );
overlay.classList.add( "visible" );
// If something goes wrong on the server, we don't want the prompt to hang
// out on the screen forever. As such, we'll auto-hide it if too much time
// has passed.
var timerCutoffAt = ( Date.now() + 10000 );
// Start watching for the existing of the reportID cookie.
var timer = setInterval(
function() {
if (
( document.cookie.indexOf( reportID ) >= 0 ) ||
( Date.now() > timerCutoffAt )
) {
overlay.classList.remove( "visible" );
clearInterval( timer );
}
},
300
);
}
</script>
</body>
</html>
Now, if we load this page in the browser and click on some of the report generation links, we get the following output:
As you can see, the overlay is only visible for the duration of the report-generation. Once the report has been generated, and the unique tracer cookie has been set, the setInterval()
timer see it, hides the prompt, and then kills the timer.
Now that I've got this working (as a proof-of-concept) in Lucee CFML, I'm not sure how much I even like the user-experience (UX). A big part of me feels like opening up the report in a new browser tab and then letting the browser manage the tab's existence is actually a better solution (especially since it allows the user to open multiple tabs / reports at one time). That said, this was a fun ColdFusion and JavaScript experiment; and, an interesting technique to have squirreled away in my back of my brain.
Want to use code from this post? Check out the license.
Reader Comments
Pretty cool. Would work well with a loading.gif also, which I'd prefer over the modal so that you can download multiple reports without extra tabs.