3

We are trying to use mod_xsendfile with Apache to efficiently handle large file downloads (> 1 GB). After installation the configuration looks like:

<IfModule mod_xsendfile.c>
<Directory "/abs_path/to/dl">
    XSendFile on
    XSendFilePath /abs_path/to/files_dir
</Directory> 
</IfModule>

The download script does nothing fancy, just checks for the existence of file to download and sets headers as per documentation, like this:

header("Content-type: application/octet-stream");
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header("X-Sendfile: " . $file);

Uninterrupted downloads work fine with any user agent we've tested with, and HTTP-Range does work fine with all but Firefox (tested versions 27 and 28). Firefox can pause a download, but resuming fails every time.

These are the http headers captured with Live HTTP headers extension:

Initial download:

http://www.oursite.com/dl/test-xs.php?ID=TestFileID

GET /dl/test-xs.php?ID=TestFileID HTTP/1.1
Host: www.oursite.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-gb,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie: some cookie string...
Connection: keep-alive

HTTP/1.1 200 OK
Date: Tue, 25 Mar 2014 10:22:46 GMT
Server: Apache
X-Powered-By: PHP/5.3.28
Content-Disposition: attachment; filename="TestFile.ext"
Last-Modified: Sun, 02 Mar 2014 18:20:36 GMT
Content-Length: 84406272
Connection: close
Content-Type: application/octet-stream

... and when Firefox tries to resume:

http://www.oursite.com/dl/test-xs.php?ID=TestFileID

GET /dl/test-xs.php?ID=TestFileID HTTP/1.1
Host: www.oursite.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-gb,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie: same cookie string...
Connection: keep-alive
Range: bytes=11238434-
If-Unmodified-Since: Sun, 02 Mar 2014 18:20:36 GMT

... the server returns a 404

HTTP/1.1 404 Not Found
Date: Tue, 25 Mar 2014 10:23:03 GMT
Server: Apache
X-Powered-By: PHP/5.3.28, PHP/5.3.28
Content-Disposition: attachment; filename="TestFile.ext"
X-Sendfile: /abs_path/to/files_dir/TestFile.ext
X-Pingback: http://www.oursite.com/xmlrpc.php
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Cache-Control: no-cache, must-revalidate, max-age=0
Pragma: no-cache
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8

... which obviously causes Firefox to fail resuming the download (which can now only be cancelled and restarted from beginning).

Considering everything works as expected in other browsers and download managers we've tried:

  1. Has anybody experienced similar behavior?
  2. Can anyone explain or point out the potential cause of it in our download script or configuration code?

Edit:

After doing some more testing it turned out that the issue is down to If-Unmodified-Since header being sent by Firefox when resuming the download. Even though this header is correctly set to the value of Last-Modified response header received from Apache, the server does not like it for some reason and responds with 404. After stripping the If-Unmodified-Since header from request by changing the .htaccess:

<Files test-xs.php>
  RequestHeader unset If-Unmodified-Since
</Files>

... resume works fine everywhere including Firefox.

This method of course is not correct if the file to download has been modified in the mean time, but it does the job for us because we use a different convention for serving newer versions of the same file.

This obviously feels more like a hack than a correct implementation, so not being sure if this should be marked as an answer, I'm leaving it as addition to original question.

New questions obviously arise:

  • Is there a better way to overcome this issue?
  • Is this a bug in mod_xsendfile?
madhatter
  • 41
  • 2

2 Answers2

1

@alternize's solution is mostly correct, but the provided snippet:

SetEnvIf Range .+ HAS_RANGE_HEADER
RequestHeader unset If-Range env=!HAS_RANGE_HEADER
RequestHeader unset If-Unmodified-Since env=!HAS_RANGE_HEADER

will not actually unset the specified headers for requests containing a Range header.

The '!' negates the match, so the above snippet will actually unset those headers for all requests except those with Range headers.

To properly remove the If-Range and If-Unmodified-Since headers from requests with a Range header, you can use same directives, but remove the negations as such:

SetEnvIf Range .+ HAS_RANGE_HEADER
RequestHeader unset If-Range env=HAS_RANGE_HEADER
RequestHeader unset If-Unmodified-Since env=HAS_RANGE_HEADER

This has been confirmed on apache 2.2.15.

Community
  • 1
  • 1
0

the mod_xsendfile does not seem to work well with some of the caching headers sent by the browsers. for example, chrome sends an "If-Range" header when resuming a download with a "Range" header:

first request headers:

GET /foo.webm

Range: bytes=0-

first response headers:

206 Partial Content

Content-Length: 54376097
Content-Range: bytes 0-54376096/54376097
Content-Type: video/webm
ETag: "78976c9d1a595cba56e24bec6f2f1178"

=> video begins to play fine

now the user scrubs to a later position in the file

second request headers:

GET /foo.webm

If-Range: "78976c9d1a595cba56e24bec6f2f1178"
Range: bytes=54373845-54376096

second response headers:

200 OK

Content-Length: 54376097
ETag: "78976c9d1a595cba56e24bec6f2f1178"

=> the entire file is sent instead of the requested byte range

workaround

unset the problematic request headers if a "Range" header exists:

SetEnvIf Range .+ HAS_RANGE_HEADER
RequestHeader unset If-Range env=!HAS_RANGE_HEADER
RequestHeader unset If-Unmodified-Since env=!HAS_RANGE_HEADER
alternize
  • 131
  • 4