Description
Problem
HttpCache doesn't refresh stale responses containing an ETag
and is also not able to increment the Age
header of the cached response.
Affected Version
I tested the Problem on v3.1
, but it seems to be around since a long time.
Scenarios
Scenario with an ETag
1st Call, Cleared Cache:
$ curl -I -XGET http://SOMEHOST/SOMEURL
HTTP/1.1 200 OK
Date: Wed, 20 Jul 2016 13:19:05 GMT
Server: Apache/2.4.18 (Ubuntu)
X-Powered-By: PHP/5.6.20-1+deb.sury.org~trusty+1
Cache-Control: public, s-maxage=60
Vary: Origin,Access-Control-Request-Headers,Access-Control-Request-Method
Allow: GET
ETag: "6d5b1b67"
X-Content-Digest: enfb4805c151e8bb9dc08ca4464c1184d1cabd0ead565196dfb2b8e1af6b669ee3
Age: 0
X-Symfony-Cache: GET /v1/videos/12/comments: miss, store
Content-Length: 1825
Content-Type: application/hal+json
2nd Call after a few seconds (not the Age
which is still 0):
$ curl -I -XGET http://SOMEHOST/SOMEURL
HTTP/1.1 200 OK
Date: Wed, 20 Jul 2016 13:19:10 GMT
Server: Apache/2.4.18 (Ubuntu)
X-Powered-By: PHP/5.6.20-1+deb.sury.org~trusty+1
Cache-Control: public, s-maxage=60
vary: Origin,Access-Control-Request-Headers,Access-Control-Request-Method
allow: GET
etag: "6d5b1b67"
x-content-digest: enfb4805c151e8bb9dc08ca4464c1184d1cabd0ead565196dfb2b8e1af6b669ee3
Age: 0
X-Symfony-Cache: GET /v1/videos/12/comments: fresh
content-length: 1825
Content-Type: application/hal+json
The cache will not expire and the response will never be refreshed.
Scenario without an ETag
1st Call, Cleared Cache:
$ curl -I -XGET http://SOMEHOST/SOMEURL
HTTP/1.1 200 OK
Date: Wed, 20 Jul 2016 13:18:48 GMT
Server: Apache/2.4.18 (Ubuntu)
X-Powered-By: PHP/5.6.20-1+deb.sury.org~trusty+1
Cache-Control: public, s-maxage=60
Vary: Origin,Access-Control-Request-Headers,Access-Control-Request-Method
Allow: GET
X-Content-Digest: enfb4805c151e8bb9dc08ca4464c1184d1cabd0ead565196dfb2b8e1af6b669ee3
Age: 0
X-Symfony-Cache: GET /v1/videos/12/comments: miss, store
Content-Length: 1825
Content-Type: application/hal+json
2nd Call after a few seconds (not the Age
which increments):
$ curl -I -XGET http://SOMEHOST/SOMEURL
HTTP/1.1 200 OK
Date: Wed, 20 Jul 2016 13:18:57 GMT
Server: Apache/2.4.18 (Ubuntu)
X-Powered-By: PHP/5.6.20-1+deb.sury.org~trusty+1
Cache-Control: public, s-maxage=60
vary: Origin,Access-Control-Request-Headers,Access-Control-Request-Method
allow: GET
x-content-digest: enfb4805c151e8bb9dc08ca4464c1184d1cabd0ead565196dfb2b8e1af6b669ee3
Age: 5
X-Symfony-Cache: GET /v1/videos/12/comments: fresh
content-length: 1825
Content-Type: application/hal+json
Cause of the Problem
The Date
header of the original request is set by accident by calling this function:
https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php#L426
if ($response->isCacheable()) { // isCacheable sets the date if it is not already set.
$this->store($request, $response);
}
isCacheable
will set set date by calling isFresh
, getTtl
, getAge
, getDate
. This however does not happen if there is a ETag
header.
Solution
Instead of relying on a date header which may be set by accident by a getter method, the date should explicitly be set for all cached responses. I'd propose to add the Date
header if not already set in the method HttpCache::store
.
protected function store(Request $request, Response $response)
{
// Change Start
if(!$response->headers->has('Date')) {
$response->setDate(new DateTime());
}
// Change End
try {
$this->store->write($request, $response);
$this->record($request, 'store');
$response->headers->set('Age', $response->getAge());
} catch (\Exception $e) {
$this->record($request, 'store-failed');
if ($this->options['debug']) {
throw $e;
}
}
// now that the response is cached, release the lock
$this->store->unlock($request);
}
Workaround for now
$response->setEtag($hash);
// @TODO: Remove as soon as an official PATCH exists for issue https://github.com/symfony/symfony/issues/19390
$response->setDate(new DateTime());