mod_xsendfile Firefox relance le problème

Nous essayons d’utiliser mod_xsendfile avec Apache pour gérer efficacement les téléchargements de fichiers volumineux (> 1 Go). Après l’installation, la configuration ressemble à:

  XSendFile on XSendFilePath /abs_path/to/files_dir   

Le script de téléchargement ne fait rien de compliqué, il suffit de vérifier l’existence d’un fichier à télécharger et de définir les en-têtes comme indiqué dans la documentation, comme ceci:

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

Les téléchargements ininterrompus fonctionnent bien avec tous les agents utilisateur que nous avons testés avec, et HTTP-Range fonctionne correctement avec tous sauf Firefox (versions testées 27 et 28). Firefox peut suspendre un téléchargement, mais sa reprise échoue à chaque fois.

Voici les en-têtes http capturés avec l’extension des en-têtes HTTP Live:

Téléchargement initial:

 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 ssortingng... 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 

… et quand Firefox essaie de reprendre:

 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 ssortingng... Connection: keep-alive Range: bytes=11238434- If-Unmodified-Since: Sun, 02 Mar 2014 18:20:36 GMT 

… le serveur retourne un 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 

… ce qui évite manifestement à Firefox de reprendre le téléchargement (qui ne peut maintenant être annulé et redémarré que depuis le début).

Considérant que tout fonctionne comme prévu dans d’autres navigateurs et gestionnaires de téléchargement, nous avons essayé:

  1. Quelqu’un at-il connu un comportement similaire?
  2. Quelqu’un peut-il expliquer ou indiquer la cause potentielle de cela dans notre script de téléchargement ou notre code de configuration?

Modifier :

Après avoir fait quelques tests supplémentaires, il s’est avéré que le problème est dû à l’en If-Unmodified-Since tête If-Unmodified-Since envoyé par Firefox lors de la reprise du téléchargement. Même si cet en-tête est correctement défini sur la valeur de l’en Last-Modified tête de réponse Last-Modified reçu d’Apache, le serveur ne l’aime pas pour certaines raisons et répond avec 404 . Après avoir If-Unmodified-Since tête If-Unmodified-Since de la requête en modifiant le .htaccess:

  RequestHeader unset If-Unmodified-Since  

… CV fonctionne bien partout, y compris Firefox.

Cette méthode n’est bien sûr pas correcte si le fichier à télécharger a été modifié entre-temps, mais il fait le travail pour nous car nous utilisons une convention différente pour servir les nouvelles versions du même fichier.

Cela ressemble évidemment plus à un piratage qu’à une implémentation correcte, donc, ne pas être sûr que cela devrait être marqué comme une réponse, je le laisse en plus de la question originale.

De nouvelles questions se posent évidemment:

  • Existe-t-il un meilleur moyen de résoudre ce problème?
  • Est-ce un bug dans mod_xsendfile?

La solution de @alternize est généralement correcte, mais l’extrait de code fourni:

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

ne supprimera pas réellement les en-têtes spécifiés pour les requêtes contenant un en-tête Range .

Le ‘ ! ‘annule la correspondance, de sorte que l’extrait ci-dessus supprime réellement ces en-têtes pour toutes les requêtes, à l’ exception de celles contenant des en-têtes de Range .

Pour supprimer correctement les en If-Range têtes If-Range et If-Unmodified-Since des demandes avec un en-tête Range , vous pouvez utiliser les mêmes directives, mais en supprimant les négations en tant que telles:

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

Cela a été confirmé sur apache 2.2.15.

Le fichier mod_xsendfile ne semble pas fonctionner correctement avec certains en-têtes de cache envoyés par les navigateurs. Par exemple, chrome envoie un en-tête “If-Range” lors de la reprise d’un téléchargement avec un en-tête “Range”:

En-têtes de première requête:

 GET /foo.webm Range: bytes=0- 

En-têtes de première réponse:

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

=> la vidéo commence à bien jouer

à présent, l’utilisateur passe à une position ultérieure dans le fichier

les en-têtes de deuxième requête:

 GET /foo.webm If-Range: "78976c9d1a595cba56e24bec6f2f1178" Range: bytes=54373845-54376096 

deuxièmes en-têtes de réponse:

 200 OK Content-Length: 54376097 ETag: "78976c9d1a595cba56e24bec6f2f1178" 

=> le fichier entier est envoyé à la place de la plage d’octets demandée

solution de contournement

désélectionner les en-têtes de demande problématiques s’il existe un en-tête “Range”:

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