Force Download with HTTP Headers


Modern web browsers support a plethora of plugins to allow content to be displayed inline; once upon a time your mp3 and pdf files would prompt a download box, now more-often than not they are displayed inside the browser.

While no one is arguing that the advancements of inline content displayed in web browsers are not without merit (okay, maybe you can tone down some of the flash...), a webmaster may occasionally wish to force a download box to pop up for one reason or another.

Through the use of the HTTP headers you can force a web browser to download content, instead of displaying it. You could even set the filename differently than the requested URI.

HTTP Headers

An HTTP response contains two parts: The response headers and the response body. The response body in this case is the content of the file. The response headers are a key-value list describing to a web-browser what to do with the content.

The header we are using is Content-Disposition. The Content-Disposition header can indicate that the response body is to be downloaded as an actual file, as opposed to displayed in the browser. Other headers, such as Content-Type can dictate the type of file the response body is -- for instance a text/html document or an audio/mpeg for an MP3, or Last-Modified: the last time the response body was modified.

If you were to click http://symkat.com/downloads/troll.gif you would be forced to download the image, instead of having the image displayed in your browser as typically happens when you open an image file on the Internet.

Let's take a look at an HTTP transaction that tells us to do this:


$ curl -I http://symkat.com/downloads/troll.gif
HTTP/1.1 200 OK
__Content-Disposition: attachment__
Content-Type: image/gif
Accept-Ranges: bytes
ETag: "-1721348577"
Last-Modified: Thu, 14 Oct 2010 06:16:59 GMT
Content-Length: 2613
Date: Thu, 14 Oct 2010 06:17:12 GMT
Server: lighttpd/1.4.19

In this example, the specific header line is Content-Disposition: attachment.

If you were to click on http://symkat.com/downloads/awesome.png the file would be downloaded as happy.png instead of awesome.png. Let's take a look at the headers to see why:


$ curl -I http://symkat.com/downloads/awesome.png
HTTP/1.1 200 OK
__Content-Disposition: attachment; filename="happy.png"__
Content-Type: image/png
Accept-Ranges: bytes
ETag: "-424443061"
Last-Modified: Thu, 14 Oct 2010 06:22:10 GMT
Content-Length: 42705
Date: Thu, 14 Oct 2010 06:26:22 GMT
Server: lighttpd/1.4.19

In this download the ; filename="happy.png" forces the file to use this name. It is worth noting that while the name of the file can be chosen programmatically with the HTTP headers, the directory path itself may not be.

We'll take a look now at how the header can be added. Please see the codeblock for your programming language or web server:

Setting the headers in PHP

Without Filename


header("Content-Disposition: Attachment")

With Filename


header("Content-Disposition: Attachment;filename=__FILENAME_HERE__")

Setting the headers in Catalyst

Without Filename


$c->res->header('Content-Disposition' => 'Attachment');

With Filename


$c->res->header('Content-Disposition' => 'Attachment; filename=__FILENAME_HERE__');

Setting the headers in Perl/Plack (Plack::Request)

Without Filename


my $res = $req->new_response(200);
$res->header('Content-Disposition' => 'Attachment');

With Filename


$res->header('Content-Disposition' => 'Attachment; filename=__FILENAME_HERE__');

Setting the headers in lighttpd

Setting headers in lighttpd can be accomplished with the use of setenv. In this example we'll force all files from the path /download/. to be downloaded. We'll also force any file which ends in /awesome.png and begins in /download/ to be downloaded with the filename happy.png specifically to illustrate the use of nested setenv statements.


$HTTP["url"] =~ "^/downloads/.*" {
    setenv.add-response-header = ( "Content-Disposition" => "attachment" )
    $HTTP["url"] =~ "/awesome.png$" {
        setenv.add-response-header = ( "Content-Disposition" => "attachment; filename=\"happy.png\"")
    }
}

Also See:

Understanding HTTP CachingCookieless Domains

Pertinent RFCs:

  • RFC 2616 19.5.1 Content-Disposition
  • RFC 2183 2.2 The Attachment Disposition Type

  • Contact Me