AlbinSoft

El fichero que no se descargaba completo.

Hoy hemos vivido un Expediente X. Teníamos un fichero .xlsx que se generaba correctamente y si lo descargabas vía FTP podías abrirlo correctamente pero si lo descargabas vía diferentes Navegadores Web entonces el Calc de Libre Office lo habría bien pero el Excel de MS Office mostraba una advertencia.

En ocasiones interesa que la descarga de un documento no sea un enlace directo al fichero sino un enlace a un script PHP para generar la información en tiempo real o para validar que el usuario identificado en el sistema está solicitando un archivo que le pertenece (una factura, unas tarifas, …).

Lógicamente el cliente solo te dice que él no puede abrirlo bien, luego comienza la tanda de todas pruebas que se te ocurren para encontrar pistas.

Una de esas pruebas ha sido descargar un ZIP en vez del Excel y nos ha llevado a otra advertencia diferente porque WinRar decía que el fichero estaba incompleto. Así es como nos hemos dado cuenta de que tamaño en el disco eran 3 bytes menos que su tamaño en el servidor.

Descartando que fuera un error del navegador porque ya se había probado con varios, hay que pensar que el problema está en la forma en que fuerzas la descarga desde PHP, aunque esencialmente hubiéramos utilizado el mismo código de siempre.

A la hora de forzar una descarga solemos recurrir a este fragmento de código más o menos común aunque cada uno considera incluir o excluir ciertas cabeceras como pudieran ser las destinadas a la caché, yo -sin ánimo de spoiler- no suelo incluir el “Content-Length”:


$filename = basename($filepath);
$filesize = filesize($filepath);
$mimetype = mime_content_type($filepath);
header("Content-Length: {$filesize}");
header("Content-Type: {$mimetype}");
header("Content-Disposition: attachment; filename={$filename}");
header("Pragma: nocache");
header("Expires: 0");
header("Cache-Control: nocache");
readfile($filepath);
die();

Ese código estará en millones de páginas webs y a nosotros mismos nos ha funcionado en docenas de proyectos desde tiempos inmemorables, cuando lo usábamos siempre que hubiera que forzar una descarga porque aún no se había normalizado el atributo “download” dentro de un anchor.

En un momento dado ha surgido la curiosidad de ¿qué tres bytes? ¿los primeros, los últimos, …? Utilizado un Visualizador de ficheros en formato Hexadecimal y hemos  visto que casualmente los tres últimos bytes eran 00 00 00, tres bytes nulos que algo estaba decidiendo obviar.

Aún indicando en el código la cantidad exacta de bytes que debía medir el fichero a descargar, o bien PHP o bien Nginx estaban decidiendo no guardarlos en el disco.

Al final por un error derivado de hacer pruebas desesperadas, en un intento en que me había dejado tanto un “readfile” como un “file_get_contents”, he llegado a la solución: devolver 1 byte más de los necesarios de manera que el contenido devuelto pasa el “trim” sin perder ningún byte pero cuando es escrito en disco solo se escriben los bytes indicados en “Content-Length” dejando sin escribir el byte extra.


readfile($filepath);
die('x');

Situaciones así me recalcan lo importante es que es tener las herramientas adecuadas en este caso el saber que existen visualizadores de ficheros en hexadecimal y también la importancia de saber siempre un poco más de lo que es estrictamente tu trabajo en este caso ser capaces de utilizar herramientas que no son el navegador para descargar un fichero con estas dos jugadas en una vez sabes que se estaban filtrando tres bytes nulos es mucho más fácil imaginarse que el problema e intentar encontrar una solución.