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?
4

2 回答 2

1

@alternize 的解决方案大部分是正确的,但提供的片段:

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

实际上不会取消设置包含标头的请求的指定标Range头。

' ! ' 否定匹配,因此上面的代码段实际上会为所有请求取消设置这些标头,但带有标头的请求除外Range

要从带有标头的请求中正确删除If-Range和标头,您可以使用相同的指令,但删除否定:If-Unmodified-SinceRange

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

这已在 apache 2.2.15 上得到证实。

于 2016-12-02T17:06:15.810 回答
0

mod_xsendfile 似乎不适用于浏览器发送的某些缓存标头。例如,当使用“Range”标头恢复下载时,chrome 会发送“If-Range”标头:

第一个请求标头:

GET /foo.webm

Range: bytes=0-

第一个响应标头:

206 Partial Content

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

=> 视频开始正​​常播放

现在用户在文件中擦洗到后面的位置

第二个请求头:

GET /foo.webm

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

第二个响应头:

200 OK

Content-Length: 54376097
ETag: "78976c9d1a595cba56e24bec6f2f1178"

=> 发送整个文件而不是请求的字节范围

解决方法

如果存在“范围”标头,则取消设置有问题的请求标头:

SetEnvIf Range .+ HAS_RANGE_HEADER
RequestHeader unset If-Range env=!HAS_RANGE_HEADER
RequestHeader unset If-Unmodified-Since env=!HAS_RANGE_HEADER
于 2014-12-23T14:36:07.470 回答