Browser Redisplaying Old PDFs: How to Manage Caching of Dynamically Generated Documents

Last year, I shared an approach for serving PDFs or other content over SSL from a Grails application in a way that wouldn’t fail due to browser caching constraints.

We have used the same approach in several of our controllers to share PDF reports of different kinds, and recently, we encountered a related problem: when users requested different PDFs in a short enough time frame, their browser was serving up cached versions of previous PDFs. To the user, it appeared like the web application was sending them an unrelated report.

Once I identified the problem as browser caching – which was not immediately evident! – I developed an approach to solve the problem.

First, I shortened the amount of time the browser was instructed to cache the content. In my previous example, the browser was instructed to keep the downloaded PDF for 5 minutes:


response.setHeader("Cache-Control", "private")
Calendar cal = Calendar.getInstance()
cal.add(Calendar.MINUTE,5)
response.setDateHeader("Expires", cal.getTimeInMillis())

I changed the time to 10 seconds.

I also reasoned that if the filename of each PDF is unique, the browser will not mistake a new PDF for an old one. In our code, we were assigning a fixed name to all output:

response.setHeader("Content-Disposition", "inline; filename=report.pdf")

Also, the URL was always the same – and some browsers ignore the content-disposition and name downloaded files using the URI. (E.g., http://host/contextRoot/pageName becomes pageName.pdf.)

I used a Grails hack to provide a unique URI for every file: in place of supplying a name:value parameter on the querystring (e.g., http://somepath?id=18), Grails lets you pass an id parameter on the URL itself (e.g., http://somepath/18). By including a unique value in the id parameter, even one that isn’t used by my application, we fool browsers into thinking that each PDF download is unique. In other words, http://somepath/18 gets saved as 18.pdf. I also put a unique name in the content-disposition header.

In this example from our web application, we are displaying the PDF and making it available for download by embedding it with an object tag in another html page. This is where I added the id parameter:


<object type="application/pdf"
name="showpdf" id="showpdf"
data="${createLink(action: 'getPDF', params:[id: params.unqid?.encodeAsHTML()])}"
style="width: 800px; height: 800px; border: solid 1px #cccccc;
background: url(${createLinkTo(dir: 'images', file: 'docloading.png')})
top center no-repeat;">
<p class="error">It appears that your web browser is not configured to display PDF files.
<g:link controller="userHelp" action="index">Click</g:link> for more information.</p>
</object>

Note that this object tag calls the getPDF action to supply the content of the object. Here’s the getPDF action:


def getPDF = {
try {
if (!session.pdffile) {
throw new Exception("Document path not found on session. Unable to retrieve report.")
}
def unqid = params.id
if (!unqid) {
throw new Exception("Unique report id not provided in parameters.")
}
def pdfFile = new File(session.pdffile)
def b = pdfFile.readBytes()
response.setContentType("application/pdf")
response.setHeader("Content-Disposition", "inline; filename=${unqid}.pdf")
response.setContentLength(b.length)
response.setHeader("Pragma", "")
response.setHeader("Cache-Control", "private")
Calendar cal = Calendar.getInstance()
cal.add(Calendar.SECOND, 10)
response.setDateHeader("Expires", cal.getTimeInMillis())
response.outputStream << b
} catch (Throwable t) {
log.fatal """Failure retrieving report document""".toString(), t
flash.error = "Failure retrieving report document: ${t.message}"
render(view: "errframe")
}
}

Between the shortened expiration time and providing a unique filename, I think we covered every case where a client browser might re-serve old downloads. At least, this was tested and worked on IE 8, Chrome, and Firefox 3.6.7.

[October 4, 2010] Postscript: Further experimenting has shown that Grails seems to ignore anything on the URL after the action name. I’m able to name the file anything I want using this expression for the URL to the file: ${createLink(action: 'getPDF')}/${filename}. I don’t have to resort to using an id param.

Advertisements

#browser-caching, #grails, #grails-hacks