Latest TweetsVerify any search engine or visitor via CLI Forward-Reverse Lookup perishablepress.com/cli-forwar…
Perishable Press

HTTP Headers for ZIP File Downloads

You know when you you’re working on a project and get stuck on something, so you scour the Web for solutions only to find that everyone else seems to be experiencing the exact same thing. Then, after many hours trying everything possible, you finally stumble onto something that seems to work. This time, the project was setting up a secure downloads area for Digging into WordPress. And when I finally discovered a solution, I told myself that it was definitely something I had to share here at Perishable Press.

Apparently, there is much to be desired when it comes to sending proper HTTP headers for file downloads. Different browsers (and not just IE) require different headers, and will error if not present exactly the way they expected. Confounding that equation is the fact that different file types also require specific headers. Then there are issues with sending an accurate (or should I say “acceptable”?) Content-Length headers when file compression is involved. Needless to say, finding a set of headers that works for all file types in all browsers is next to impossible. And I won’t even get into the issues involved with readfile() and large-download file-sizes.

Download Headers that actually work

After trying hundreds of different headers and combinations, I hit upon a set that works great for ZIP downloads (and other file types as well) in all tested browsers. Here’s what they look like using PHP:

<?php // HTTP Headers for ZIP File Downloads
// https://perishablepress.com/press/2010/11/17/http-headers-file-downloads/

// set example variables
$filename = "Inferno.zip";
$filepath = "/var/www/domain/httpdocs/download/path/";

// http headers for zip downloads
header("Pragma: public");
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Cache-Control: public");
header("Content-Description: File Transfer");
header("Content-type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"".$filename."\"");
header("Content-Transfer-Encoding: binary");
header("Content-Length: ".filesize($filepath.$filename));
ob_end_flush();
@readfile($filepath.$filename);
?>

This PHP script is known to work under the following conditions:

  • Operating System: Linux
  • Server: Apache/2.2.3 (CentOS)
  • MYSQL Version: 5.0.77-log
  • PHP Version: 5.2.6
  • PHP Safe Mode: Off
  • PHP Allow URL fopen: On
  • PHP Memory Limit: 256M
  • PHP Max Upload Size: 2M
  • PHP Max Post Size: 8M
  • PHP Max Script Execute Time: 30s

With this code, the downloads work in the following tested browsers:

  • Firefox 3.0, 3.5 (Mac & PC)
  • Opera 8, 9, 10 (Mac & PC)
  • Internet Explorer 7, 8
  • Chrome 7.0.517
  • Camino 2
  • Safari 5 (PC)
  • Safari 3 (Mac)

The downloads work for the following types of files (including small and large file types):

  • .zip
  • .txt
  • .pdf
  • .jpg

Obviously, I didn’t test every file type in every browser, but the positive results from those listed here suggest a much wider range of files and browsers that will work. For the file sizes, I tested small files only a few bytes in length, and also large files up to around 20MB or so. Also would like to give a shout to the Live HTTP Headers extension for Firefox. It proved indispensable throughout the troubleshooting/testing/pulling-my-hair-out process.

As always, if you can contribute to the content of this post with further information about sending proper HTTP Headers for file downloads, you may just save a life ;)

Jeff Starr
About the Author Jeff Starr = Creative thinker. Passionate about free and open Web.
Archives
25 responses
  1. Hey Jeff,

    Great article, I’m going to study these headers very closely. I’ve just been working on some file-download stuff, and had to go thru all of this myself.

    One thing I found interesting: I’m running gzip on my server, compressing stuff as it’s sent. I found that a a very limited subset of Windows machines running XP SP3 would blow up the download- they’d claim that the zip was corrupted even if it was actually fine.

    The solution, i discovered after a lot of searching and testing, was to disable gzip for .zip file types – then IE doesn’t get confused : ) Technically I don’t think you’re supposed to be compressing archives in transit anyway, but IE was the only browser I found that had any issue with it.

    Thanks again!

    Trav

  2. Almost the exact same code I use.

  3. Jeff Starr

    @Travis: Yes, I experienced issues with file compression and zip files, and discovered the same thing: disabling gzip for .zip files prevents corrupted downloads in Internet Explorer. Definitely good to know, that one :)

    @Skye: Almost, eh? What are the differences, if you don’t mind sharing your secret recipe ;)

  4. @ Jeff. I’ve used this on a couple sites: http://www.damnsemicolon.com/php/force-download-a-file-automatically-and-keeping-statistics-with-php

    Only real difference is the output:

    ob_clean();
    flush();
    readfile($file);
    ob_clean();
    flush();
    exit;

    Can’t remember what kind of issues I was having when I came up with that but seems kinda like overkill now.

  5. Hi Jeff ,

    As far as I know, the Content-Description and Content-Transfer-Encoding headers are for MIME, not HTTP, so not needed here.

    The second Cache-Control header is going to overwrite the first – default PHP header() behaviour – so you should do header("Cache-Control: public, must-revalidate, post-check=0, pre-check=0"); instead.

    I haven’t (to date/yet!?) experienced any problems with serving gzipped .zip files, even to little ol’ IE6. But I have experienced other problems with IE6 when one doesn’t cache the file (Expires: 0) and the user clicks “open” instead of “save”. I’ve given a solution here.

    When compressing files before sending (using PHP), I sometimes got memory-related errors for huge files. So what I now do (in addition to the general/cache headers) is something like the following:

    Specify the gzip-related headers :

    header("Content-Encoding: gzip");
    header('Vary: Accept-Encoding');

    Then if the file is smaller than 5MB :

    $content = gzencode(file_get_contents($file), 6);
    header("Content-Length: " . strlen($content));
    echo $content;

    But for bigger files, I first create a gzip file on the server (gzopen) by reading chunks of the original file at a time (fread) and adding each chunk to the gzip file (gzwrite), which then becomes the file I send to the user:

    $file = GZip::create($file_orig);
    header("Content-Length: " . filesize($file));
    readfile($file);

    Works for me. Hope it helps!

  6. Hi Jeff,

    Before sending any headers I would do this:

    while (ob_get_level()) {
         ob_end_clean();
    }

    This will discard any buffered output that may have been generated and ensure that no extra bytes are sent before the file’s content.

    ***

    Personally I prefer to use lightweight servers (like nginx) – this allows to use “magic” headers like X-Accel-Redirect: /path/to/file – making nginx handle the download, not PHP (and as a free bonus, the download can be resumeable as nginx can handle Range: headers).

    For lighttpd there is SendFile header (not sure if I wrote it correctly), for Apache there is a mod_sendfile extension that allows to use X-Sendfile header.

  7. Please stop using made-up HTTP headers.

    HTTP protocol supports only binary, so Content-Transfer-Encoding: binary is needed only in fantasy-land.

    Content-Description: File Transfer is another wishful thinking header.

    How about reading HTTP spec?

  8. i guess, i used this code once, but i forgot which project. anyway, thanks for sharing the code!

  9. Jeff Starr

    @ Abraham, Vladimir, kl: Thanks for the feedback — it is good to hear from people who actually know what they’re doing :)

    Once I get things fine-tuned and spend a few years reading HTTP spec, I’ll post again with any improvements made to the code.

    Thanks again.

  10. You also might want to check out http://joseph.randomnetworks.com/archives/2004/10/01/making-ie-accept-file-downloads/

    IE refuses to download files over SSL (HTTPS) without
    session_cache_limiter('private');

    What I did was this:
    //Fix Internet explorer file downloads for HTTPS...
    if(strpos(GetVar($_SERVER,'HTTP_USER_AGENT'), 'MSIE'))
         session_cache_limiter('private');

  11. @kl, well I read the http spec and couldn’t find the Content-Description header but I saw numerous examples with it (including the php.net site) so I’m not sure where everyone is getting that from.

  12. Joshua McGee November 25, 2010 @ 12:29 am

    I don’t need to code this yet, but good grief is this getting a bookmark. I would bet Wall Street to a piggybank that I’ll need this some day.

    Thank you!

[ Comments are closed for this post ]