Grails File-Serving Controller and Tomcat SSL Problem

April 21, 2009 at 4:15 pm 4 comments

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.

Advertisement

Entry filed under: Development. Tags: , , , , , .

42 Things You Never Want to See in Your Data Center

4 Comments Add your own

  • 1. ssl certificates  |  May 15, 2009 at 11:53 am

    That sounds like a good solution, to override the headers that Tomcat appends. Keep us posted on how that works out for you.

    Reply
    • 2. msilverboard  |  May 15, 2009 at 12:48 pm

      Thanks. It’s working – I’ve used this code in three controllers now.

      Reply
  • 3. Nevin Pick  |  October 23, 2009 at 9:50 am

    This solved our problem downloading files over SSL. Excellent solution! Thanks!

    Reply
  • [...] 26, 2010 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 [...]

    Reply

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.