Grails File-Serving Controller and Tomcat SSL Problem

I’ve been working on a Grails application to serve up PDFs and other files, and I’ve run into a documented issue involving Internet Explorer and Apache Tomcat with SSL. I’ll explain the solution I implemented.

First of all, here’s the code I was using to serve files from a Grails controller. The list action of the controller is pretty standard – it fetches a list of documents from the database, using some selection criteria, and then it displays the documents on list.gsp, with links to the show action that pass the document’s id as a parameter.


class BulletinsController {

def list = {
List bulletins = Bulletin.getBulletins()
[params:params, bulletins:bulletins]
}

def show = {

def bulletin = bulletinService.getBulletinById(Integer.parseInt(params.id))

String fullPath =
bulletin.rootPath + "/" +
bulletin.filename
File file = new File(fullPath)
byte[] content = file.readBytes()

OutputStream out = response.getOutputStream();

// Set headers
response.setContentType(bulletin.contentType)
response.setContentLength(content.size())
response.setHeader("Content-disposition", "attachment; filename=${bulletin.filename}")

// Write the file content
out.write(content)
out.close()
}

}

This code works as expected in Firefox 3. But on Internet Explorer 7, it resulted in an error message, and no file being received:

Internet Explorer cannot download %filename% from %hostname%.

Internet Explorer cannot download filename from hostname.

Internet Explorer was not able to open this Internet site. The requested site is either unavailable or cannot be found. Please try again later.

Researching this message turned up Microsoft KB article 323308, which explains that when Internet Explorer tries to open a file using a third-party application like Adobe Reader, it has to temporarily cache the file somewhere so that the other application can access it. If the remote web server serving the file has specified Cache-control=no-cache in the HTTP response header, IE will honor this directive and will not cache the file — which causes a problem, since the third-party application won’t have a file to read.

I also found numerous mentions of a somewhat unexpected behavior of Apache Tomcat: When you define a security constraint for a resource, making it CONFIDENTIAL, Tomcat not only encrypts the resource with SSL, it also automatically appends cache control headers for it.

I confirmed that this was happening with my application by uncommenting the RequestDumperValve line in my Tomcat conf/server.xml and restarting Tomcat.

<Valve className="org.apache.catalina.valves.RequestDumperValve"/>

The next time I tried to download a PDF, Tomcat recorded the response headers in catalina.log:

Apr 21, 2009 3:36:10 PM org.apache.catalina.valves.RequestDumperValve invoke
INFO: contentLength=196923
Apr 21, 2009 3:36:10 PM org.apache.catalina.valves.RequestDumperValve invoke
INFO: contentType=application/pdf
Apr 21, 2009 3:36:10 PM org.apache.catalina.valves.RequestDumperValve invoke
INFO: header=Pragma=No-cache
Apr 21, 2009 3:36:10 PM org.apache.catalina.valves.RequestDumperValve invoke
INFO: header=Cache-Control=no-cache
Apr 21, 2009 3:36:10 PM org.apache.catalina.valves.RequestDumperValve invoke
INFO: header=Expires=Wed, 31 Dec 1969 19:00:00 EST
Apr 21, 2009 3:36:10 PM org.apache.catalina.valves.RequestDumperValve invoke
INFO: header=Content-disposition=attachment; filename=CFG-09-2012.pdf

There are ways to disable this behavior in Tomcat’s config. However, I think that this is a desirable behavior most of the time, and I’m only modifying it at all because of Internet Explorer’s problem. So my solution is to simply override the headers that Tomcat appends by appending my own in this particular Grails controller action. Here’s the working version of the show action:


def show = {
def bulletin = bulletinService.getBulletinById(Integer.parseInt(params.id))

String fullPath =
bulletin.rootPath + "/" +
bulletin.filename
File file = new File(fullPath)
byte[] content = file.readBytes()

OutputStream out = response.getOutputStream();

// Set headers
response.setContentType(bulletin.contentType)
response.setContentLength(content.size())
response.setHeader("Content-disposition", "attachment; filename=${bulletin.filename}")

/* By default, Tomcat will set headers on any SSL content to deny
* caching. This will cause downloads to Internet Explorer to fail. So
* we override Tomcat's default behavior here. */
response.setHeader("Pragma", "")
response.setHeader("Cache-Control", "private")
Calendar cal = Calendar.getInstance()
cal.add(Calendar.MINUTE,5)
response.setDateHeader("Expires", cal.getTimeInMillis())

// Write the file content
out.write(content)
out.close()
}

Setting the Pragma header to an empty string takes care of user-agents implementing HTTP 1.0. Setting the Cache-control header to private does the same for HTTP 1.1. And setting the Expires header to a value five minutes in the future instead of in the past should take care of any user-agents that use the expiration as a cache-control convention.

Advertisements

#cache-control, #grails, #groovy, #internet-explorer, #ssl, #tomcat