Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Using A Tracer Cookie To Watch For Browser Download-Prompts In Lucee CFML 5.3.6.61

By Ben Nadel on

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.

Third Attempt: Tracer Cookie and setInterval()

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> &raquo;
	</p>
	<p>
		This report does some medium processing (<strong>3,000 ms</strong>).
		<a href="./report.cfm?delay=3000" class="report">Run report</a> &raquo;
	</p>
	<p>
		This report does some heavy processing (<strong>5,000 ms</strong>).
		<a href="./report.cfm?delay=5000" class="report">Run report</a> &raquo;
	</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:

Download prompt being monitoring using unique cookie in Lucee CFML.

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.



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.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
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.