Supporting Conditional GET in PHP
This site’s RSS feeds now support Conditional GET. Since the feeds are dynamically generated on every request, adding support took a bit of hacking around with PHP. Here’s the function I came up with (based on the excellent description provided by Charles Miller in the article linked above):
function doConditionalGet($timestamp) {
// A PHP implementation of conditional get, see
// http://fishbowl.pastiche.org/archives/001132.html
$last_modified = substr(date('r', $timestamp), 0, -5).'GMT';
$etag = '"'.md5($last_modified).'"';
// Send the headers
header("Last-Modified: $last_modified");
header("ETag: $etag");
// See if the client has provided the required headers
$if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ?
stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']) :
false;
$if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ?
stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) :
false;
if (!$if_modified_since && !$if_none_match) {
return;
}
// At least one of the headers is there - check them
if ($if_none_match && $if_none_match != $etag) {
return; // etag is there but doesn't match
}
if ($if_modified_since && $if_modified_since != $last_modified) {
return; // if-modified-since is there but doesn't match
}
// Nothing has changed since their last request - serve a 304 and exit
header('HTTP/1.0 304 Not Modified');
exit;
}
Usage is simple: Work out the timestamp that the page content was last modified and call doConditionalGet($timestamp);. It will send the 304 header for you and exit if the client claims to have seen the content already—otherwise control will return to your main script and you can serve content as normal. Slightly inelegant, but it does the job.
Unfortunately I don’t have a Conditional-GET supporting RSS aggregator to hand so I have no idea if it works or not (so far I’ve only tested it by watching the headers sent with LiveHTTPHeaders). I’d be grateful if someone could confirm that this has had the desired effect.
Update: I’ve changed the above code sample (and my implementation) to send the ETag header as ETag rather than etag.
I submitted code to do this to the HTTP PEAR module maintainer last year, but my email was ignored. I think the best place to do it is in Apache anyway - see what the client and mod_php provide, and if they match, terminate the process and send the 304. That solves the problem on a wide scale, across multiple languages.
Jim - 23rd April 2003 17:33 - #
Phil Ringnalda - 23rd April 2003 17:44 - #
Sam Ruby - 23rd April 2003 19:07 - #
Simon Willison - 23rd April 2003 19:26 - #
It doesn't work with my homegrown aggregator. There are two problems.
The first is that your etag header is being sent as "etag" and not "ETag". My HTTP Client (from PEAR) implementation is case sensitive, so your etag header is not seen.
However, your "Last-Modified" header is being seen. Unfortunately, my implementation is partially broken and is sending a blank If-None-Match header to you. I should actually send nothing at all. However, since I am sending a blank header, your implementation is seeing that, and therefore, your conditional ($if_none_match && $if_none_match != $etag) is applying to my case, and so you are sending me the document again.
If either one of us would fix our ends, it would work for me. However, since we're both slightly, and only slightly, broken... it doesn't.
I'll fix my end.
Reverend Jim - 23rd April 2003 22:06 - #
Reverend Jim - 23rd April 2003 22:10 - #
Simon Willison - 23rd April 2003 23:05 - #
Mark Nottingham - 24th April 2003 02:31 - #
Thanks Mark, looks good.
Jim - 24th April 2003 03:19 - #
kellan - 24th April 2003 04:22 - #
Bill Kearney - 26th April 2003 15:42 - #
Sam Ruby - 26th April 2003 16:30 - #
Ian Evans - 20th August 2003 09:00 - #
Hey, old posting but i discovered a 'little' bug...
You are using
$last_modified = substr(date('r', $timestamp), 0, -5).'GMT';to get $last_modified. date('r') returns only 1 digit for the day sometimes, and this seems to be a problem, the header will be "Jan, 01 1970 ..."Instead it would be better to use the following:
$last_modified = gmdate("D, d M Y H:i:s", $timestamp).' GMT';Jan Piotrowski - 9th April 2004 14:48 - #
WRT Reverend Jim's comment, treating HTTP headers as case-sensitive isn't just slightly broken, what if every header was in a different case to what your client expects?
However the blank If-Modified-Since header raises a good point about conditional GET code, if any error occurs in the conditional GET code or if the If-Modified_Since is invalid in some way then it's important that you treat it as if there was no If-Modified-Since (or If-None-Match) header and do the normal 200 OK response. Doing so is always safe in this context and can be used as a short cut (so you don't have to accept all 3 of the datetime formats RFC 2616 says you should accept).
If you are doing something like this and you can't be sure that the page that would be generated at that time isn't octet-to-octet identical to the one that was generated earlier then you should probably use weak ETags.
Jon Hanna - 7th May 2004 13:54 - #
gmdate()function in PHP to return a GMT date. In your case:$last_modified = gmdate('D, d M Y H:i:s \G\M\T', $timestamp);Alex - 29th June 2004 01:14 - #
tim - 21st July 2004 19:47 - #