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

July 26, 2010 at 1:03 pm Leave a comment

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.

Advertisement

Entry filed under: Development. Tags: , , .

Split a PDF into pages with PdfStamper (iText) Material Spam: Opting Out of AJC Reach

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Trackback this post  |  Subscribe to the comments via RSS Feed


Categories

Enter your email address to subscribe to this blog and receive notifications of new posts by email.

Join 1 other follower


Follow

Get every new post delivered to your Inbox.