Search code examples
phpcachingdownloadhttp-headerslast-modified

Last modified header for a fixed resource link where the resource changes from time to time?


Currently I am in the process of writing a little WordPress plugin for download resources with running updates. That means I call a fixed endpoint URL like:

https://www.example.com/downloads/app/latest

If this URL is called, the server is delivering the most recent version of the requested resource (e.g. an executable application). The name of the resource may vary but it may also be the same as an older version. Therefore I want to put a working last modified header.

Basically I got the headers to set from this question: Force file download with php using header()

$quoted = sprintf('"%s"', addcslashes(basename($file), '"\\'));
$size   = filesize($file);

header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . $quoted); 
header('Content-Transfer-Encoding: binary');
header('Connection: Keep-Alive');
header('Pragma: public');
header('Content-Length: ' . $size);

Now I wonder how to set the headers correctly such that the browser knows if the resource has changed or not. Assuming that I have the last modified date of the requested resource my guess is:

header('Cache-Control: must-revalidate');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s T', $last_modified_time));

In case I have no idea what the last modified date is I would put:

header('Cache-Control: no-cache');
header('Expires: 0');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s T'));

Is this correct (especially the headers) or am I missing out on something? Not sure because of the large amount of settings for cache control, modification date, expires and maybe other though.


Solution

  • After some research I have found the ETag header which is solving my problem. Setting the ETag header works as described here: How to use etags in a PHP file?

    Also found some information on how to combine this with the Expires header: ETag vs Header Expires - the ETag header makes at least one request to compare the file checksum while the expires header makes at least one request at all.

    Last part of the puzzle was: HTTP: Does the ETag header make the Cache-Control header obsolete? How to make sure Cache-Control is not harmful then?

    Therefore I have the perfect answer now (at least I think):

    $quoted = sprintf('"%s"', addcslashes(basename($file), '"\\'));
    $size   = filesize($file);
    $etag   = hash_file('md5', $file); 
    
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename=' . $quoted); 
    header('Content-Transfer-Encoding: binary');
    header('Connection: Keep-Alive');
    header('Pragma: public');
    header('Content-Length: ' . $size);
    header('Cache-Control: no-cache');
    header('Expires: 0');
    header('ETag: ' . $etag);