Testing a 304 response code implementation in PHP - php

I have an API and I've been trying to add cache control headers to it.
The API already makes use of PhpFastCache for server side caching but I wanted to add an additional layer of browser control caching. I came across this intelligent php cache control page and modified it slightly.
Using PhpFastCache, I do a check to see if the server side cache exists, if it doesn't then query the DB and output normally with a 200 response code. If the cache does exist then I do the following:
//get the last-modified-date of this very file
$lastModified=filemtime(__FILE__);
//get a unique hash of this file (etag)
$etagFile = md5( $CachedString->get() );
//get the HTTP_IF_MODIFIED_SINCE header if set
$ifModifiedSince=(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : false);
//get the HTTP_IF_NONE_MATCH header if set (etag: unique file hash)
$etagHeader=(isset($_SERVER['HTTP_IF_NONE_MATCH']) ? trim($_SERVER['HTTP_IF_NONE_MATCH']) : false);
//set last-modified header
header("Last-Modified: ".gmdate("D, d M Y H:i:s", $lastModified)." GMT");
//set etag-header
header("Etag: $etagFile");
//make sure caching is turned on
header('Cache-Control: public');
//check if page has changed. If not, send 304 and exit
if (#strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])==$lastModified || $etagHeader == $etagFile)
{
header("HTTP/1.1 304 Not Modified");
exit;
}else{
//Cache Match - Output Cache Result
header('Content-Type: application/json');
echo $CachedString->get();
}
I'm using this line to get the cached response as md5:
$etagFile = md5( $CachedString->get() );
Then doing a check to see if this md5 content has changed:
if (#strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])==$lastModified || $etagHeader == $etagFile)
{
header("HTTP/1.1 304 Not Modified");
exit;
}else{
//Cache Match - Output Cache Result
header('Content-Type: application/json');
echo $CachedString->get();
}
However I can never seem to get the 304 response header. It is ALWAYS a 200 code response header.
curl -I -L https://db.ygoprodeck.com/api/v7/cardinfo.php?name=Tornado%20Dragon
With the response always being:
HTTP/1.1 200 OK
Date: Tue, 17 Mar 2020 13:37:31 GMT
Content-Type: application/json
Connection: keep-alive
Set-Cookie: __cfduid=daaab295934a2a8ef966c2c70fe0955b91584452250; expires=Thu, 16-Apr-20 13:37:30 GMT; path=/; domain=.ygoprodeck.com; HttpOnly; SameSite=Lax
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
Cache-Control: public
Last-Modified: Tue, 17 Mar 2020 13:15:53 GMT
Etag: 399b9ba2d69ab115f46faa44be04d0ca
Vary: User-Agent
CF-Cache-Status: DYNAMIC
Expect-CT: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
Server: cloudflare
CF-RAY: 57571be8a986a72f-DUB

Your request is being proxied through Cloudflare which has its own caching layer. If you test this direct to origin/with a grey clouded record are you getting a 304?
You said you were working on browser caching, browser is going to cache based on the max-age setting you send, but don't see one being set in the response.

Related

Browser ignoring html/php cache headers?

I am confused about cache headers.
I am trying to make the browser cache my php/html pages because they are mostly static and they would not change for a month or so.
The urls look like: example.com/article-url and in background that is a php page like /article_url.php
I tried this in PHP:
$cache_seconds = 60*60*24*30;
header("Expires: ".gmdate('D, d M Y H:i:s \G\M\T', time()+$cache_seconds));
header("Cache-Control:public, max-age=".$cache_seconds);
And in browser debug window I can see that indeed the page would expire next month:
Request URL: https://www.example.com/article-url
Request Method: GET
Status Code: 200
Referrer Policy: no-referrer-when-downgrade
cache-control: public, max-age=2592000
content-encoding: gzip
content-length: 2352
content-type: text/html
date: Mon, 25 Nov 2019 14:23:40 GMT
expires: Wed, 25 Dec 2019 14:23:40 GMT
server: nginx
status: 200
vary: Accept-Encoding
But if I access that page again, I see it was generated again, I made it print the request timestamp in footer, and I can see the page is generated again on each page load.
I was expecting browser to show exact same page from cache.
What am I doing wrong ?

Phalcon, HTTP Cache of a generated image

I'm writing a web service who generate thumbs of images with Phalcon.
I try to HTTP cache it.
This is my code :
$seconds = 43200;
$expireDate = new DateTime();
$expireDate->modify("+ $seconds seconds");
$finfo = new finfo(FILEINFO_MIME_TYPE);
$app->response->setHeader('Content-Type', 'Content-type: ' . $finfo->buffer($data));
$app->response->setExpires($expireDate);
$app->response->setHeader('Pragma', 'cache');
$app->response->setHeader('Cache-Control', "private, max-age=$seconds");
$app->response->setHeader('E-Tag', md5(filemtime($path)));
$app->response->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', filemtime($path)).' GMT');
$app->response->sendHeaders();
echo $data;
The image is corretly displayed. But when you refresh it, the http code is always 200, I try on another image of another website and I've got 200, 304, 304, 304...
This is my raw response header :
HTTP/1.1 200 OK
Date: Thu, 27 Aug 2015 18:38:41 GMT
Server: Apache/2.4.10 (Debian)
Expires: Fri, 28 Aug 2015 06:38:41 GMT
Pragma: cache
Cache-Control: private, max-age=43200
E-Tag: 501a8d62f276eb5b165b8a709bf4e5b4
Last-Modified: Sun, 05 Jul 2015 20:34:14 GMT
Keep-Alive: timeout=5, max=90
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: image/jpeg
Someone see what i'm doing wrong ?
Thanks in advance.
Your php code needs to return the 304 Not Modified header when the browser asks if the cached image is still valid. Put an if statement at the top of your script to handle that request before sending the image again.
You are always sending the image that's why the browser is showing a 200 response.
If you add the max-age to the last-modified date you get an expiry time in the past.
Your code is a mess of every possible way to influence caching (and btw http does not define a "pragma: cache" header). As to the question of what you should be doing, that depends on what you are trying to acheive - just getting load off the server, faster page rendering or caching up to apre-planned replacement or something else. And you haven't told us what this is.
Thanks PaulS !
$filemtimeOk = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $filemtime <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
$etagOk = isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $etag;
if ($filemtimeOk && $etagOk) {
$app->response->setStatusCode(304, "Not Modified");
$app->response->sendHeaders();
} else {
// Normal case... (send data and headers)
}

Symfony2 Reverse proxy has a cache control private

I'm playing with Symfony2 Reverse proxy and HTTP cache and I had a lot of read on the subject.
However I'm getting stuck on how it works in my case.
Here is a use case.
GET /api/articles returns something like:
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Type: application/json
Set-Cookie: PHPSESSID=12345; expires=Thu, 14-Nov-2013 14:50:35 GMT; path=/
age: 0
allow: GET, POST
cache-control: must-revalidate, no-cache, private
etag: "da4b6c4f1540a12a112936e58db06df8c95fd3c4"
vary: Accept,Accept-Encoding
x-content-digest: enbf30f962b06f99bd91843741537e112fbd3300c8
x-symfony-cache: GET /api/articles: miss, store
As you can see there the Cache-Control header is marked as private along with no-cache & must-revalidate. However, I think I'm setting the Response correctly:
$response = clone $view->getResponse();
$response
->setPublic()
->setEtag($etag)
->setSharedMaxAge(60)
->setVary(array('Accept'))
;
if ($response->isNotModified($this->getRequest())) {
return $response;
}
I set it to Public so it should work. You may have noticed the Set-Cookie header, I dunno if it matters, but as long as I set the cache as public it shouldn't, isn't it?
Now, if I GET /api/articles with an If-None-Match: {etag} I get a 304 which is correct, but the Cache-Control header is the same.
Note that if I disable the Reverse Proxy, the Cache-Control is correct and showing me:
Cache-Control: public, s-maxage=60 which is what I except.

Smarty IE9 requests file download of index.php (after an upload has not occured)? is it my code at fault or activecollab?

Okay so I'm getting this weird unexpected response from Internet Explorer, while testing file upload with smarty in php.
Here my smarty code for file upload (view), simplified down to main issue, for those who have not used activecollab the Router::assemble is just forming a url with parameters that are read from the MVC.
(source: iforce.co.nz)
<div id="xero_invoice_manager_api">
{form action=Router::assemble('xero_invoice_manager_api') method=post id="xero_invoice_manager" enctype="multipart/form-data"}
<div class="content_stack_wrapper">
<input type="file" name="file_1" /><br/>
<input type="file" name="file_2" /><br/>
{wrap_buttons}
{submit success_event="api_updated" }Authenticate{/submit}
{/wrap_buttons}
{/form}
</div></div>
And here is my jquery for the view.
App.Wireframe.Events.bind('api_event_finished.content', function(event, settings) {
App.Wireframe.Flash.success(App.lang('Xero Invoice Manager has saved/uploaded your Xero API data.'));
});
Here is my simplified controller (I have found the issue is with smarty and not php).
//api view
function api(){
if ( $this->request->isSubmitted()) {
$this->response->respondWithData(true);
}
}
Here is my controller with the upload occuring..
//api view
function api(){
$this->assignSmarty();
if ($this->request->isSubmitted()) {
$this->XeroAuthUpdate(); //update everything
if(isset($_FILES)){
$file_manager = new XeroFileManager();
$file_manager->dumpFiles($_FILES);
//upload the files
foreach($_FILES as $file){
$file_manager->handle_certificate_file($file);
} //foreach add the headers
if(function_exists('headers_list')){
xeroDebugMode("[Controller] the headers to be sent are... ", headers_list());
} //function check
} //end if
$this->response->respondWithData(array(
// constraints
'key_result' => (bool)$this->checkValue(XeroAuths::getSetting('xero_consumer')),
'secret_result' => (bool)$this->checkValue(XeroAuths::getSetting('xero_secret')),
// files secruity certificates
'publickey' => (bool)file_exists(XERO_PUBLIC_KEY_PATH),
'privatekey' => (bool)file_exists(XERO_PRIVATE_KEY_PATH),
'xero_auth' => (bool)validateXeroAuth(),
//login constraints
'install' => !$this->checkInstallRequirements(),
));
} //close the request
}
Here is a response from firefox with the file_1 and file_2 not empty.
(source: iforce.co.nz)
Here is a response from internet explorer 9 with file_1 and file_2 empty (so far so good).
(source: iforce.co.nz)
Here is the problematic response from internet explorer 9 with file_1 (i.e. publickey.cer) and file_2 (i.e. privatekey.pem) not empty (download index.php huh?).
(source: iforce.co.nz)
My response from activecollab
Hello Micheal,
Sorry for the late reply.
Unfortunately we cannot figure out where the problem is. It looks like everything is written OK but without dealing with the code itself there's pretty much nothing we can do. Dealing with JSON responses in IE works fine across activeCollab (well, not in IE6) since almost everything in aC 3 is based on JSON, which makes your issue specific and probably there's something wrong in your code.
Regards,
Oliver Maksimovic
activeCollab development & support
General and Pre-Sale Questions: 1-888-422-6260 (toll-free) Technical Support: support#activecollab.com
An associate has suggested..
Would suggest trying the following though:
1) open IE -> open the developer tools (press F12) -> Click "Cache" in menu -> click "Clear Browser Cache"... When thats finished click "Cache" and then click "Always refresh from server".
this forces IE to not cache anything, as I've had numerous times where IE was caching ajax requests and causing some very strange behaviour.
let me if this fixes your problem, and if so we can add some php to your ajax response to force all browsers to never cache the response. otherwise if that still doesn't work, probably need to do some JS debugging in IE, to see what's being sent and compare it to your FF firebug results.
headers_sent() comes up blank
but the headers_list (just before respondWithData is called), for Internet Explorer.
2012-08-08 06:50:16 the headers sent from this request is... Array
(
[0] => X-Powered-By: PHP/5.3.8
[1] => Set-Cookie: ac_activeCollab_sid_yhRk0xSZku=1%2Fhkykz0Rw0796e4lDykXekNXvhMMxC8pV4akJPMvA%2F2012-08-08+06%3A50%3A15; expires=Wed, 22-Aug-2012 06:50:15 GMT; path=/
[2] => Content-Type: application/json
[3] => Expires: Mon, 26 Jul 1997 05:00:00 GMT
[4] => Cache-Control: no-cache, no-store, must-revalidate
[5] => Pragma: no-cache
)
Response Headers from Raw tab on Fiddler, on Internet Explorer
HTTP/1.1 200 OK
Date: Sat, 11 Aug 2012 08:08:46 GMT
Server: Apache/2.2.21 (Win32) mod_ssl/2.2.21 OpenSSL/1.0.0e PHP/5.3.8 mod_perl/2.0.4 Perl/v5.10.1
X-Powered-By: PHP/5.3.8
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: ac_activeCollab_sid_yhRk0xSZku=11%2Fz8rWxiRchAh8EWinYO2d7a1mmvn2DMKUdse1vfKh%2F2012-08-11+0 8%3A08%3A46; expires=Sat, 25-Aug-2012 08:08:46 GMT; path=/
Content-Length: 107
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/json; charset=utf-8
{"key_result":true,"secret_result":true,"publickey":true,"privatekey":true,"xero_auth":true,"install":true}
Response Headers from Raw tab on Firefox.
HTTP/1.1 200 OK
Date: Sat, 11 Aug 2012 08:13:45 GMT
Server: Apache/2.2.21 (Win32) mod_ssl/2.2.21 OpenSSL/1.0.0e PHP/5.3.8 mod_perl/2.0.4 Perl/v5.10.1
X-Powered-By: PHP/5.3.8
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: ac_activeCollab_sid_yhRk0xSZku=12%2FO40CbXC9Vfa7OVnderlK2MFnvnpkyeckvO0Ab5NQ%2F2012-08-11+08%3A13%3A45; expires=Sat, 25-Aug-2012 08:13:45 GMT; path=/
Content-Length: 107
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/json; charset=utf-8
{"key_result":true,"secret_result":true,"publickey":true,"privatekey":true,"xero_auth":true,"install":true}
Any ideas on what I'm doing wrong with IE? and why Internet Explorer is notifying the user to download index.php (when the fields are active with values). Keeping in mind that no actual uploading is occurring on the server-side (during the initial test, the index.php download request is irrelevant to move_uploaded_file).
It could be that IE specific code has an error, and so the returned content-type is different. If you make an AJAX request for some kind of XML or JSON data and instead get some kind of file HTML error response with a different content-type or disposition than expected, the browser might not know what to do with it.
You might want to find a way to view or log the response (as opposed to request) headers sent by the web server. Usually a prompt for file download comes from a content-disposition header... though in this case it might just be because it's a file coming from an asynchronous request.
You might also want to look at:
IE prompts to open or save json result from server
and
How can I convince IE to simply display application/json rather than offer to download it?
I had a similar issue using pupload and mvc3. I know we use different technology but maybe my issue could help you. I had this:
public JsonResult UploadDoc(string correlationId)
{
try
{
//upload code here
return Json(new { message = "chunk uploaded", name = "test" });
}
catch (Exception ex)
{
return Json(new { message = "chunk uploaded", name = "test" });
}
}
Now everytime I wanted to try upload a file I would get IE asking me to open or download a file which just contained that json response above. If I set my return type as "String" and set my return code as:
return "{\"respCode\" : \"200\", \"Msg\" : \"succussful\",\"mimeType\": \"" + Request.Files[0].ContentType + "\", \"fileSize\": \"" + Request.Files[0].ContentLength + "\"}";
Then the file was successfully uploaded. Response Header for when it failed: "Content-Type: application/json; charset=utf-8" . Response Header for when it works with "String" return type:
"Content-Type: text/html; charset=utf-8". Hope it helps, cheers.
Due to the lack of answers, I think I need to take a different approach in my jquery.. until an actual solution is found.

What headers do I want to send together with a 304 response?

When I send a 304 response. How will the browser interpret other headers which I send together with the 304?
E.g.
header("HTTP/1.1 304 Not Modified");
header("Expires: " . gmdate("D, d M Y H:i:s", time() + $offset) . " GMT");
Will this make sure the browser will not send another conditional GET request (nor any request) until $offset time has "run out"?
Also, what about other headers?
Should I send headers like this together with the 304:
header('Content-Type: text/html');
Do I have to send:
header("Last-Modified:" . $modified);
header('Etag: ' . $etag);
To make sure the browser sends a conditional GET request the next time the $offset has "run out" or does it simply save the old Last Modified and Etag values?
Are there other things I should be aware about when sending a 304 response header?
This blog post helped me a lot in order to tame the "conditional get" beast.
An interesting excerpt (which partially contradicts Ben's answer) states that:
If a normal response would have included an ETag header, that header must also be included in the 304 response.
Cache headers (Expires, Cache-Control, and/or Vary), if their values might differ from those sent in a previous response.
This is in complete accordance with the RFC 2616 sec 10.3.5.
Below a 200 request...
HTTP/1.1 200 OK
Server: nginx/0.8.52
Date: Thu, 18 Nov 2010 16:04:38 GMT
Content-Type: image/png
Last-Modified: Thu, 15 Oct 2009 02:04:11 GMT
Expires: Thu, 31 Dec 2010 02:04:11 GMT
Cache-Control: max-age=315360000
Accept-Ranges: bytes
Content-Length: 6394
Via: 1.1 proxyIR.my.corporate.proxy.name:8080 (IronPort-WSA/6.3.3-015)
Connection: keep-alive
Proxy-Connection: keep-alive
X-Junk: xxxxxxxxxxxxxxxx
...And its optimal valid 304 counterpart.
HTTP/1.1 304 Not Modified
Server: nginx/0.8.52
Date: Thu, 18 Nov 2010 16:10:35 GMT
Expires: Thu, 31 Dec 2011 16:10:35 GMT
Cache-Control: max-age=315360000
Via: 1.1 proxyIR.my.corporate.proxy.name:8080 (IronPort-WSA/6.3.3-015)
Connection: keep-alive
Proxy-Connection: keep-alive
X-Junk: xxxxxxxxxxx
Notice that the Expires header is at most Current Date + One Year as per RFC-2616 14.21.
The Content-Type header only applies to responses which contain a body. A 304 response does not contain a body, so that header does not apply. Similarly, you don't want to send Last-Modified or ETag because a 304 response means that the document hasn't changed (and so neither have the values of those two headers).
For an example, see this blog post by Anne van Kesteren examining WordPress' http_modified function. Note that it returns either Last-Modified and ETag or a 304 response.

Categories