Skip to content

Commit 6f9878d

Browse files
aschemppfabpot
authored andcommitted
[HttpKernel] Bugfix/last modified response strategy
1 parent 8eb64bd commit 6f9878d

File tree

2 files changed

+36
-14
lines changed

2 files changed

+36
-14
lines changed

HttpCache/ResponseCacheStrategy.php

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class ResponseCacheStrategy implements ResponseCacheStrategyInterface
3737
private int $embeddedResponses = 0;
3838
private bool $isNotCacheableResponseEmbedded = false;
3939
private int $age = 0;
40+
private \DateTimeInterface|null|false $lastModified = null;
4041
private array $flagDirectives = [
4142
'no-cache' => null,
4243
'no-store' => null,
@@ -90,6 +91,11 @@ public function add(Response $response)
9091
$expires = $response->getExpires();
9192
$expires = null !== $expires ? (int) $expires->format('U') - (int) $response->getDate()->format('U') : null;
9293
$this->storeRelativeAgeDirective('expires', $expires >= 0 ? $expires : null, 0, $isHeuristicallyCacheable);
94+
95+
if (false !== $this->lastModified) {
96+
$lastModified = $response->getLastModified();
97+
$this->lastModified = $lastModified ? max($this->lastModified, $lastModified) : false;
98+
}
9399
}
94100

95101
/**
@@ -102,17 +108,16 @@ public function update(Response $response)
102108
return;
103109
}
104110

105-
// Remove validation related headers of the master response,
106-
// because some of the response content comes from at least
107-
// one embedded response (which likely has a different caching strategy).
111+
// Remove Etag since it cannot be merged from embedded responses.
108112
$response->setEtag(null);
109-
$response->setLastModified(null);
110113

111114
$this->add($response);
112115

113116
$response->headers->set('Age', $this->age);
114117

115118
if ($this->isNotCacheableResponseEmbedded) {
119+
$response->setLastModified();
120+
116121
if ($this->flagDirectives['no-store']) {
117122
$response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate');
118123
} else {
@@ -122,6 +127,8 @@ public function update(Response $response)
122127
return;
123128
}
124129

130+
$response->setLastModified($this->lastModified ?: null);
131+
125132
$flags = array_filter($this->flagDirectives);
126133

127134
if (isset($flags['must-revalidate'])) {
@@ -162,17 +169,14 @@ private function willMakeFinalResponseUncacheable(Response $response): bool
162169
// RFC2616: A response received with a status code of 200, 203, 300, 301 or 410
163170
// MAY be stored by a cache […] unless a cache-control directive prohibits caching.
164171
if ($response->headers->hasCacheControlDirective('no-cache')
165-
|| $response->headers->getCacheControlDirective('no-store')
172+
|| $response->headers->hasCacheControlDirective('no-store')
166173
) {
167174
return true;
168175
}
169176

170-
// Last-Modified and Etag headers cannot be merged, they render the response uncacheable
177+
// Etag headers cannot be merged, they render the response uncacheable
171178
// by default (except if the response also has max-age etc.).
172-
if (\in_array($response->getStatusCode(), [200, 203, 300, 301, 410])
173-
&& null === $response->getLastModified()
174-
&& null === $response->getEtag()
175-
) {
179+
if (null === $response->getEtag() && \in_array($response->getStatusCode(), [200, 203, 300, 301, 410])) {
176180
return false;
177181
}
178182

Tests/HttpCache/ResponseCacheStrategyTest.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@
55
*
66
* (c) Fabien Potencier <fabien@symfony.com>
77
*
8-
* This code is partially based on the Rack-Cache library by Ryan Tomayko,
9-
* which is released under the MIT license.
10-
* (based on commit 02d2b48d75bcb63cf1c0c7149c077ad256542801)
11-
*
128
* For the full copyright and license information, please view the LICENSE
139
* file that was distributed with this source code.
1410
*/
@@ -138,6 +134,28 @@ public function testMainResponseWithExpirationIsUnchangedWhenThereIsNoEmbeddedRe
138134
$this->assertTrue($mainResponse->isFresh());
139135
}
140136

137+
public function testLastModifiedIsMergedWithEmbeddedResponse()
138+
{
139+
$cacheStrategy = new ResponseCacheStrategy();
140+
141+
$embeddedDate = new \DateTime('-1 hour');
142+
143+
// This master response uses the "validation" model
144+
$masterResponse = new Response();
145+
$masterResponse->setLastModified(new \DateTime('-2 hour'));
146+
$masterResponse->setEtag('foo');
147+
148+
// Embedded response uses "expiry" model
149+
$embeddedResponse = new Response();
150+
$embeddedResponse->setLastModified($embeddedDate);
151+
$cacheStrategy->add($embeddedResponse);
152+
153+
$cacheStrategy->update($masterResponse);
154+
155+
$this->assertTrue($masterResponse->isValidateable());
156+
$this->assertSame($embeddedDate->getTimestamp(), $masterResponse->getLastModified()->getTimestamp());
157+
}
158+
141159
public function testMainResponseIsNotCacheableWhenEmbeddedResponseIsNotCacheable()
142160
{
143161
$cacheStrategy = new ResponseCacheStrategy();

0 commit comments

Comments
 (0)