<h1 dir="auto">Problem</h1>
<p dir="auto">HttpCache doesn't refresh stale responses containing an <code class="notranslate">ETag</code> and is also not able to increment the <code class="notranslate">Age</code> header of the cached response.</p>
<h1 dir="auto">Affected Version</h1>
<p dir="auto">I tested the Problem on <code class="notranslate">v3.1</code>, but it seems to be around since a long time.</p>
<h1 dir="auto">Scenarios</h1>
<h2 dir="auto">Scenario with an <code class="notranslate">ETag</code></h2>
<p dir="auto">1st Call, Cleared Cache:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="$ 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: &quot;6d5b1b67&quot;
X-Content-Digest: enfb4805c151e8bb9dc08ca4464c1184d1cabd0ead565196dfb2b8e1af6b669ee3
Age: 0
X-Symfony-Cache: GET /v1/videos/12/comments: miss, store
Content-Length: 1825
Content-Type: application/hal+json"><pre class="notranslate"><code class="notranslate">$ 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
</code></pre></div>
<p dir="auto">2nd Call after a few seconds (not the <code class="notranslate">Age</code> which is still 0):</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="$ 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: &quot;6d5b1b67&quot;
x-content-digest: enfb4805c151e8bb9dc08ca4464c1184d1cabd0ead565196dfb2b8e1af6b669ee3
Age: 0
X-Symfony-Cache: GET /v1/videos/12/comments: fresh
content-length: 1825
Content-Type: application/hal+json"><pre class="notranslate"><code class="notranslate">$ 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
</code></pre></div>
<p dir="auto"><em>The cache will not expire and the response will never be refreshed.</em></p>
<h1 dir="auto">Scenario without an <code class="notranslate">ETag</code></h1>
<p dir="auto">1st Call, Cleared Cache:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="$ 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"><pre class="notranslate"><code class="notranslate">$ 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
</code></pre></div>
<p dir="auto">2nd Call after a few seconds (not the <code class="notranslate">Age</code> which increments):</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="$ 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"><pre class="notranslate"><code class="notranslate">$ 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
</code></pre></div>
<h1 dir="auto">Cause of the Problem</h1>
<p dir="auto">The <code class="notranslate">Date</code> header of the original request is set by accident by calling this function:<br>
<a href="https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php#L426">https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php#L426</a></p>
<div class="highlight highlight-text-html-php notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="if ($response-&gt;isCacheable()) {  // isCacheable sets the date if it is not already set.
    $this-&gt;store($request, $response);
}"><pre class="notranslate"><span class="pl-k">if</span> (<span class="pl-s1"><span class="pl-c1">$</span>response</span>-&gt;<span class="pl-en">isCacheable</span>()) {  <span class="pl-c">// isCacheable sets the date if it is not already set.</span>
    <span class="pl-s1"><span class="pl-c1">$</span><span class="pl-smi">this</span></span>-&gt;<span class="pl-en">store</span>(<span class="pl-s1"><span class="pl-c1">$</span>request</span>, <span class="pl-s1"><span class="pl-c1">$</span>response</span>);
}</pre></div>
<p dir="auto"><code class="notranslate">isCacheable</code> will set set date by calling <code class="notranslate">isFresh</code>, <code class="notranslate">getTtl</code>, <code class="notranslate">getAge</code>, <code class="notranslate">getDate</code>. This however does not happen if there is a <code class="notranslate">ETag</code> header.</p>
<h1 dir="auto">Solution</h1>
<p dir="auto">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 <code class="notranslate">Date</code> header if not already set in the method <code class="notranslate">HttpCache::store</code>.</p>
<p dir="auto"><a href="https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php#L583">https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php#L583</a></p>
<div class="highlight highlight-text-html-php notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="protected function store(Request $request, Response $response)
{
    // Change Start
    if(!$response-&gt;headers-&gt;has('Date')) {
        $response-&gt;setDate(new DateTime());
    }
    // Change End
    try {
        $this-&gt;store-&gt;write($request, $response);
        $this-&gt;record($request, 'store');
        $response-&gt;headers-&gt;set('Age', $response-&gt;getAge());
    } catch (\Exception $e) {
        $this-&gt;record($request, 'store-failed');
        if ($this-&gt;options['debug']) {
            throw $e;
        }
    }
    // now that the response is cached, release the lock
    $this-&gt;store-&gt;unlock($request);
}"><pre class="notranslate"><span class="pl-k">protected</span> <span class="pl-k">function</span> <span class="pl-en">store</span>(<span class="pl-smi"><span class="pl-smi">Request</span></span> <span class="pl-s1"><span class="pl-c1">$</span>request</span>, <span class="pl-smi"><span class="pl-smi">Response</span></span> <span class="pl-s1"><span class="pl-c1">$</span>response</span>)
{
    <span class="pl-c">// Change Start</span>
    <span class="pl-k">if</span>(!<span class="pl-s1"><span class="pl-c1">$</span>response</span>-&gt;<span class="pl-c1">headers</span>-&gt;<span class="pl-en">has</span>(<span class="pl-s">'Date'</span>)) {
        <span class="pl-s1"><span class="pl-c1">$</span>response</span>-&gt;<span class="pl-en">setDate</span>(<span class="pl-k">new</span> <span class="pl-v">DateTime</span>());
    }
    <span class="pl-c">// Change End</span>
    <span class="pl-k">try</span> {
        <span class="pl-s1"><span class="pl-c1">$</span><span class="pl-smi">this</span></span>-&gt;<span class="pl-c1">store</span>-&gt;<span class="pl-en">write</span>(<span class="pl-s1"><span class="pl-c1">$</span>request</span>, <span class="pl-s1"><span class="pl-c1">$</span>response</span>);
        <span class="pl-s1"><span class="pl-c1">$</span><span class="pl-smi">this</span></span>-&gt;<span class="pl-en">record</span>(<span class="pl-s1"><span class="pl-c1">$</span>request</span>, <span class="pl-s">'store'</span>);
        <span class="pl-s1"><span class="pl-c1">$</span>response</span>-&gt;<span class="pl-c1">headers</span>-&gt;<span class="pl-en">set</span>(<span class="pl-s">'Age'</span>, <span class="pl-s1"><span class="pl-c1">$</span>response</span>-&gt;<span class="pl-en">getAge</span>());
    } <span class="pl-k">catch</span> (<span class="pl-smi"><span class="pl-smi">\<span class="pl-v">Exception</span></span></span> <span class="pl-s1"><span class="pl-c1">$</span>e</span>) {
        <span class="pl-s1"><span class="pl-c1">$</span><span class="pl-smi">this</span></span>-&gt;<span class="pl-en">record</span>(<span class="pl-s1"><span class="pl-c1">$</span>request</span>, <span class="pl-s">'store-failed'</span>);
        <span class="pl-k">if</span> (<span class="pl-s1"><span class="pl-c1">$</span><span class="pl-smi">this</span></span>-&gt;<span class="pl-c1">options</span>[<span class="pl-s">'debug'</span>]) {
            <span class="pl-k">throw</span> <span class="pl-s1"><span class="pl-c1">$</span>e</span>;
        }
    }
    <span class="pl-c">// now that the response is cached, release the lock</span>
    <span class="pl-s1"><span class="pl-c1">$</span><span class="pl-smi">this</span></span>-&gt;<span class="pl-c1">store</span>-&gt;<span class="pl-en">unlock</span>(<span class="pl-s1"><span class="pl-c1">$</span>request</span>);
}</pre></div>
<h1 dir="auto">Workaround for now</h1>
<div class="highlight highlight-text-html-php notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="$response-&gt;setEtag($hash);
// @TODO: Remove as soon as an official PATCH exists for issue https://github.com/symfony/symfony/issues/19390
$response-&gt;setDate(new DateTime());"><pre class="notranslate"><span class="pl-s1"><span class="pl-c1">$</span>response</span>-&gt;<span class="pl-en">setEtag</span>(<span class="pl-s1"><span class="pl-c1">$</span>hash</span>);
<span class="pl-c">// @TODO: Remove as soon as an official PATCH exists for issue https://github.com/symfony/symfony/issues/19390</span>
<span class="pl-s1"><span class="pl-c1">$</span>response</span>-&gt;<span class="pl-en">setDate</span>(<span class="pl-k">new</span> <span class="pl-v">DateTime</span>());</pre></div>
<p dir="auto">I am debugging a Symfony 2.1 app using the HttpCache and it seems to me that in every request the age remains at 0. imho we need to set a date in the response inside <code class="notranslate">HttpCache::store()</code> before we call <code class="notranslate">write()</code> to ensure that on a subsequent request we can properly compute the age of the response.</p>