Welcome to the new design! Please report any bugs or issues, thanks :)
Web Dev + WordPress + Security

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 = Fullstack Developer. Book Author. Teacher. Human Being.
USP Pro: Unlimited front-end forms for user-submitted posts and more.

25 responses to “HTTP Headers for ZIP File Downloads”

  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
    Jeff Starr 2010/11/17 4:53 pm

    @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. abdusfauzi 2010/11/20 12:58 pm

    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 2010/11/25 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. Something to add? Let me know.
Welcome
Perishable Press is operated by Jeff Starr, a professional web developer and book author with two decades of experience. Here you will find posts about web development, WordPress, security, and more »
USP Pro: Unlimited front-end forms for user-submitted posts and more.
Thoughts
This new Admin Page Framework looks pretty good. Can't wait to check it out.
Two great places to find awesome plugins: pluginsearch.com and WP's browse new.
2 things I hate to see in stylesheets: _ and #
Love VLC media player but it fails miserably when it comes to randomizing large collections of mp3 and other files.
Dashlane redesigned, stating proudly they "removed all filigree". Should have kept it; the app now looks generic and boring. Killed your identity.
Working on integration for setaPDF + EDD on the new books subdomain. Good times.
Toggle visibility of hidden files on Mac: Cmd + Shift + .