12

I have cacheable dynamic content made in PHP 5.1.0+. I already send the correct headers (including Last-Modified and ETag) to clients.

I now want my script to be able to answer $_SERVER['HTTP_IF_MODIFIED_SINCE'] and $_SERVER['HTTP_IF_NONE_MATCH'] when present. When the conditions matches, I want to answer a HTTP 304 "Not Modified" to clients.

What are the correct conditions? When exactly I issue a 304 instead of the whole content?

The accepted answer in question How to know when to send a 304 Not Modified response seems to issue this correctly but I have hard times to port that code to PHP 5.

Thank you!

Community
  • 1
  • 1
AlexV
  • 22,658
  • 18
  • 85
  • 122

7 Answers7

33

I've always used:

function caching_headers ($file, $timestamp) {
    $gmt_mtime = gmdate('r', $timestamp);
    header('ETag: "'.md5($timestamp.$file).'"');
    header('Last-Modified: '.$gmt_mtime);
    header('Cache-Control: public');

    if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) || isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
        if ($_SERVER['HTTP_IF_MODIFIED_SINCE'] == $gmt_mtime || str_replace('"', '', stripslashes($_SERVER['HTTP_IF_NONE_MATCH'])) == md5($timestamp.$file)) {
            header('HTTP/1.1 304 Not Modified');
            exit();
        }
    }
}

Don't remember whether I wrote it or got it from somewhere else...

I'm normally using it at the top of a file in this way:

caching_headers ($_SERVER['SCRIPT_FILENAME'], filemtime($_SERVER['SCRIPT_FILENAME']));
Community
  • 1
  • 1
Rich Bradshaw
  • 71,795
  • 44
  • 182
  • 241
  • 5
    Nice function Rich! A little more legible: `caching_headers (__FILE__, filemtime(__FILE__));`. – Julian Feb 01 '12 at 18:20
  • 1
    Really helpful! Tried this but performing multiple requests give out alternatively a 200 and 304. Moving the "Last-Modified" and "Cache-Control" after the "ETag" definition fixes it. – Benjamin Intal Apr 06 '12 at 01:10
  • Would be good to check if `"HTTP_IF_NONE_MATCH"` was set as well to avoid PHP warning and PHP unknown when null is passed to stripslashes in PHP 8.1. – Taras Jul 19 '23 at 16:05
3

The answer you're referencing seems to contain all you need. To summarize:

  • generate your own ETag and Last-Modified headers, just as if you would be sending the whole body
  • look at the If-Modified-Since header the client sent, if your own last-modified is older or the same send the 304
  • look at the If-None-Match header of the client, if it matches your own ETag send the 304
  • if you reach this place, the headers did not match, send complete body and new ETag/Last-Modified headers
Wim
  • 11,091
  • 41
  • 58
2

Here is a snippet of my render_file() function.

$last_modified = filemtime($filename);
if ($last_modified === false) {
  throw new Exception('Modify date unknown');
}
if (array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER)) {
  $if_modified_since = strtotime(preg_replace('/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE']));
  if ($if_modified_since >= $last_modified) { // Is the Cached version the most recent?
    header($_SERVER['SERVER_PROTOCOL'].' 304 Not Modified');
    exit();
  }
}
header('Last-Modified: '.date('r', $last_modified)); // tz should be GMT according to specs but also works with other tzs

// other headers and contents go here  
Bob Fanger
  • 28,949
  • 7
  • 62
  • 78
  • 1
    And what about HTTP_IF_NONE_MATCH? Where whould it be fitted in your snippet? – AlexV Jan 04 '10 at 19:38
  • 1
    The modification-date was validation enough for me, calculating a Etag (checksum for the contents md5/sha1) generates some server-overhead. However, etags are less errorprone. If content correctness is important check the IF_NONE_MATCH first. If the IF_NONE_MATCH isn't set, then check the IF_MODIFIED_SINCE. Dont check the IF_MODIFIED_SINCE if the etag doesnt match. Because you know the browser cache is invalid! Just send the 304 header and exit() – Bob Fanger Jan 04 '10 at 20:44
1

If I could improve slightly on the original brilliant answer from Rich Bradshaw https://stackoverflow.com/users/16511/rich-bradshaw

This code is tweaked and now 100% passes the If-Modified-Since and If-None-Match checks. It also correctly formats the Last-Modified Date as original answer sends out +0000 on the end instead of GMT and adds the VARY header to the 304 response. You can test this at redbot.org

<?php
function caching_headers ($file, $timestamp) {
    $lastModified=filemtime($_SERVER['SCRIPT_FILENAME']);
    $gmt_mtime = gmdate("D, d M Y H:i:s T", $lastModified);
    header('ETag: "'.md5($timestamp.$file).'"');
    header('Last-Modified: '.$gmt_mtime);
    header('Cache-Control: must-revalidate, proxy-revalidate, max-age=3600');

    if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) || isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
        if ($_SERVER['HTTP_IF_MODIFIED_SINCE'] == $gmt_mtime || str_replace('"', '', stripslashes($_SERVER['HTTP_IF_NONE_MATCH'])) == md5($timestamp.$file)) {
            header('HTTP/1.1 304 Not Modified');
            header("Vary: Accept-Encoding,User-Agent");
            exit();
        }
    }
}
caching_headers ($_SERVER['SCRIPT_FILENAME'], filemtime($_SERVER['SCRIPT_FILENAME']));
?>
MitchellK
  • 2,322
  • 1
  • 16
  • 25
0

If the client has performed a conditional GET request and access is allowed, but the document has not been modified, the server SHOULD respond with this status code. The 304 response MUST NOT contain a message-body, and thus is always terminated by the first empty line after the header fields.

From - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5

So, if you send a 304 don't send the body.

mr-sk
  • 13,174
  • 11
  • 66
  • 101
  • I know that and my question is not about this... My question is about WHEN do I send a 304... – AlexV Jan 04 '10 at 19:00
  • Ah sorry, gotta slow down, = ] – mr-sk Jan 04 '10 at 19:10
  • I mean in which cases I send a 304. Example of answer I seek: "Send 304 when HTTP_IF_MODIFIED_SINCE present and match your last modified date AND when HTTP_IF_NONE_MATCH is present and match your ETags". – AlexV Jan 04 '10 at 19:11
0

This article will answer all your questions on caching

I found that adding

RewriteRule .* - [E=HTTP_IF_MODIFIED_SINCE:%{HTTP:If-Modified-Since}]
RewriteRule .* - [E=HTTP_IF_NONE_MATCH:%{HTTP:If-None-Match}]

To the bottom of my htaccess file (below all rewriterule) worked.

John Magnolia
  • 16,769
  • 36
  • 159
  • 270
-3

Why?

Having done a lot of research on the subject I found that conditional requests actually slow down a site. There are certain scenarios where that is not the case, but mapping to general usage patterns overall it results in lower throughput and less effective caching.

C.

symcbean
  • 47,736
  • 6
  • 59
  • 94
  • Why would it slow down a site. I can't even imagine how it would slow it down (it you send proper cache headers)... – AlexV Jan 06 '10 at 18:40
  • 1
    The short answer to that question is about 5 pages of text and graphs. I keep meaning to put it on the internet somewhere.....watch this space. – symcbean Jan 07 '10 at 13:17
  • Would be interesting to see that :) – AlexV Jan 07 '10 at 14:25