Skip to content

Commit fb64453

Browse files
committed
Merge branch '6.4' into 7.1
* 6.4: remove custom CSV escape character from tests [HttpKernel] ESI fragment content may be missing in conditional requests [SecurityBundle] Revert adding `_stateless` attribute to the request when firewall is stateless and the attribute is not already set fix compatibility with Twig 3.12 and 4.0 Improve and add tests for Last-Modified computation with ESI responses
2 parents ae9c137 + d4f958f commit fb64453

File tree

3 files changed

+207
-10
lines changed

3 files changed

+207
-10
lines changed

HttpCache/HttpCache.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,9 @@ public function handle(Request $request, int $type = HttpKernelInterface::MAIN_R
229229

230230
$response->prepare($request);
231231

232-
$response->isNotModified($request);
232+
if (HttpKernelInterface::MAIN_REQUEST === $type) {
233+
$response->isNotModified($request);
234+
}
233235

234236
return $response;
235237
}

Tests/HttpCache/HttpCacheTest.php

+169
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,175 @@ public function testEsiCacheSendsTheLowestTtlForHeadRequests()
12741274
$this->assertEquals(100, $this->response->getTtl());
12751275
}
12761276

1277+
public function testEsiCacheIncludesEmbeddedResponseContentWhenMainResponseFailsRevalidationAndEmbeddedResponseIsFresh()
1278+
{
1279+
$this->setNextResponses([
1280+
[
1281+
'status' => 200,
1282+
'body' => 'main <esi:include src="/foo" />',
1283+
'headers' => [
1284+
'Cache-Control' => 's-maxage=0', // goes stale immediately
1285+
'Surrogate-Control' => 'content="ESI/1.0"',
1286+
'Last-Modified' => 'Mon, 12 Aug 2024 10:00:00 +0000',
1287+
],
1288+
],
1289+
[
1290+
'status' => 200,
1291+
'body' => 'embedded',
1292+
'headers' => [
1293+
'Cache-Control' => 's-maxage=10', // stays fresh
1294+
'Last-Modified' => 'Mon, 12 Aug 2024 10:05:00 +0000',
1295+
]
1296+
],
1297+
]);
1298+
1299+
// prime the cache
1300+
$this->request('GET', '/', [], [], true);
1301+
$this->assertSame(200, $this->response->getStatusCode());
1302+
$this->assertSame('main embedded', $this->response->getContent());
1303+
$this->assertSame('Mon, 12 Aug 2024 10:05:00 +0000', $this->response->getLastModified()->format(\DATE_RFC2822)); // max of both values
1304+
1305+
$this->setNextResponses([
1306+
[
1307+
// On the next request, the main response has an updated Last-Modified (main page was modified)...
1308+
'status' => 200,
1309+
'body' => 'main <esi:include src="/foo" />',
1310+
'headers' => [
1311+
'Cache-Control' => 's-maxage=0',
1312+
'Surrogate-Control' => 'content="ESI/1.0"',
1313+
'Last-Modified' => 'Mon, 12 Aug 2024 10:10:00 +0000',
1314+
],
1315+
],
1316+
// no revalidation request happens for the embedded response, since it is still fresh
1317+
]);
1318+
1319+
// Re-request with Last-Modified time that we received when the cache was primed
1320+
$this->request('GET', '/', ['HTTP_IF_MODIFIED_SINCE' => 'Mon, 12 Aug 2024 10:05:00 +0000'], [], true);
1321+
1322+
$this->assertSame(200, $this->response->getStatusCode());
1323+
1324+
// The cache should use the content ("embedded") from the cached entry
1325+
$this->assertSame('main embedded', $this->response->getContent());
1326+
1327+
$traces = $this->cache->getTraces();
1328+
$this->assertSame(['stale', 'invalid', 'store'], $traces['GET /']);
1329+
1330+
// The embedded resource was still fresh
1331+
$this->assertSame(['fresh'], $traces['GET /foo']);
1332+
}
1333+
1334+
public function testEsiCacheIncludesEmbeddedResponseContentWhenMainResponseFailsRevalidationAndEmbeddedResponseIsValid()
1335+
{
1336+
$this->setNextResponses([
1337+
[
1338+
'status' => 200,
1339+
'body' => 'main <esi:include src="/foo" />',
1340+
'headers' => [
1341+
'Cache-Control' => 's-maxage=0', // goes stale immediately
1342+
'Surrogate-Control' => 'content="ESI/1.0"',
1343+
'Last-Modified' => 'Mon, 12 Aug 2024 10:00:00 +0000',
1344+
],
1345+
],
1346+
[
1347+
'status' => 200,
1348+
'body' => 'embedded',
1349+
'headers' => [
1350+
'Cache-Control' => 's-maxage=0', // goes stale immediately
1351+
'Last-Modified' => 'Mon, 12 Aug 2024 10:05:00 +0000',
1352+
]
1353+
],
1354+
]);
1355+
1356+
// prime the cache
1357+
$this->request('GET', '/', [], [], true);
1358+
$this->assertSame(200, $this->response->getStatusCode());
1359+
$this->assertSame('main embedded', $this->response->getContent());
1360+
$this->assertSame('Mon, 12 Aug 2024 10:05:00 +0000', $this->response->getLastModified()->format(\DATE_RFC2822)); // max of both values
1361+
1362+
$this->setNextResponses([
1363+
[
1364+
// On the next request, the main response has an updated Last-Modified (main page was modified)...
1365+
'status' => 200,
1366+
'body' => 'main <esi:include src="/foo" />',
1367+
'headers' => [
1368+
'Cache-Control' => 's-maxage=0',
1369+
'Surrogate-Control' => 'content="ESI/1.0"',
1370+
'Last-Modified' => 'Mon, 12 Aug 2024 10:10:00 +0000',
1371+
],
1372+
],
1373+
[
1374+
// We have a stale cache entry for the embedded response which will be revalidated.
1375+
// Let's assume the resource did not change, so the controller sends a 304 without content body.
1376+
'status' => 304,
1377+
'body' => '',
1378+
'headers' => [
1379+
'Cache-Control' => 's-maxage=0',
1380+
],
1381+
],
1382+
]);
1383+
1384+
// Re-request with Last-Modified time that we received when the cache was primed
1385+
$this->request('GET', '/', ['HTTP_IF_MODIFIED_SINCE' => 'Mon, 12 Aug 2024 10:05:00 +0000'], [], true);
1386+
1387+
$this->assertSame(200, $this->response->getStatusCode());
1388+
1389+
// The cache should use the content ("embedded") from the cached entry
1390+
$this->assertSame('main embedded', $this->response->getContent());
1391+
1392+
$traces = $this->cache->getTraces();
1393+
$this->assertSame(['stale', 'invalid', 'store'], $traces['GET /']);
1394+
1395+
// Check that the embedded resource was successfully revalidated
1396+
$this->assertSame(['stale', 'valid', 'store'], $traces['GET /foo']);
1397+
}
1398+
1399+
public function testEsiCacheIncludesEmbeddedResponseContentWhenMainAndEmbeddedResponseAreFresh()
1400+
{
1401+
$this->setNextResponses([
1402+
[
1403+
'status' => 200,
1404+
'body' => 'main <esi:include src="/foo" />',
1405+
'headers' => [
1406+
'Cache-Control' => 's-maxage=10',
1407+
'Surrogate-Control' => 'content="ESI/1.0"',
1408+
'Last-Modified' => 'Mon, 12 Aug 2024 10:05:00 +0000',
1409+
],
1410+
],
1411+
[
1412+
'status' => 200,
1413+
'body' => 'embedded',
1414+
'headers' => [
1415+
'Cache-Control' => 's-maxage=10',
1416+
'Last-Modified' => 'Mon, 12 Aug 2024 10:00:00 +0000',
1417+
]
1418+
],
1419+
]);
1420+
1421+
// prime the cache
1422+
$this->request('GET', '/', [], [], true);
1423+
$this->assertSame(200, $this->response->getStatusCode());
1424+
$this->assertSame('main embedded', $this->response->getContent());
1425+
$this->assertSame('Mon, 12 Aug 2024 10:05:00 +0000', $this->response->getLastModified()->format(\DATE_RFC2822));
1426+
1427+
// Assume that a client received 'Mon, 12 Aug 2024 10:00:00 +0000' as last-modified information in the past. This may, for example,
1428+
// be the case when the "main" response at that point had an older Last-Modified time, so the embedded response's Last-Modified time
1429+
// governed the result for the combined response. In other words, the client received a Last-Modified time that still validates the
1430+
// embedded response as of now, but no longer matches the Last-Modified time of the "main" resource.
1431+
// Now this client does a revalidation request.
1432+
$this->request('GET', '/', ['HTTP_IF_MODIFIED_SINCE' => 'Mon, 12 Aug 2024 10:00:00 +0000'], [], true);
1433+
1434+
$this->assertSame(200, $this->response->getStatusCode());
1435+
1436+
// The cache should use the content ("embedded") from the cached entry
1437+
$this->assertSame('main embedded', $this->response->getContent());
1438+
1439+
$traces = $this->cache->getTraces();
1440+
$this->assertSame(['fresh'], $traces['GET /']);
1441+
1442+
// Check that the embedded resource was successfully revalidated
1443+
$this->assertSame(['fresh'], $traces['GET /foo']);
1444+
}
1445+
12771446
public function testEsiCacheForceValidation()
12781447
{
12791448
$responses = [

Tests/HttpCache/ResponseCacheStrategyTest.php

+35-9
Original file line numberDiff line numberDiff line change
@@ -138,22 +138,48 @@ public function testLastModifiedIsMergedWithEmbeddedResponse()
138138
{
139139
$cacheStrategy = new ResponseCacheStrategy();
140140

141+
$mainResponse = new Response();
142+
$mainResponse->setLastModified(new \DateTimeImmutable('-2 hour'));
143+
141144
$embeddedDate = new \DateTimeImmutable('-1 hour');
145+
$embeddedResponse = new Response();
146+
$embeddedResponse->setLastModified($embeddedDate);
142147

143-
// This master response uses the "validation" model
144-
$masterResponse = new Response();
145-
$masterResponse->setLastModified(new \DateTimeImmutable('-2 hour'));
146-
$masterResponse->setEtag('foo');
148+
$cacheStrategy->add($embeddedResponse);
149+
$cacheStrategy->update($mainResponse);
150+
151+
$this->assertTrue($mainResponse->headers->has('Last-Modified'));
152+
$this->assertSame($embeddedDate->getTimestamp(), $mainResponse->getLastModified()->getTimestamp());
153+
}
154+
155+
public function testLastModifiedIsRemovedWhenEmbeddedResponseHasNoLastModified()
156+
{
157+
$cacheStrategy = new ResponseCacheStrategy();
158+
159+
$mainResponse = new Response();
160+
$mainResponse->setLastModified(new \DateTimeImmutable('-2 hour'));
147161

148-
// Embedded response uses "expiry" model
149162
$embeddedResponse = new Response();
150-
$embeddedResponse->setLastModified($embeddedDate);
163+
151164
$cacheStrategy->add($embeddedResponse);
165+
$cacheStrategy->update($mainResponse);
166+
167+
$this->assertFalse($mainResponse->headers->has('Last-Modified'));
168+
}
152169

153-
$cacheStrategy->update($masterResponse);
170+
public function testLastModifiedIsNotAddedWhenMainResponseHasNoLastModified()
171+
{
172+
$cacheStrategy = new ResponseCacheStrategy();
154173

155-
$this->assertTrue($masterResponse->isValidateable());
156-
$this->assertSame($embeddedDate->getTimestamp(), $masterResponse->getLastModified()->getTimestamp());
174+
$mainResponse = new Response();
175+
176+
$embeddedResponse = new Response();
177+
$embeddedResponse->setLastModified(new \DateTimeImmutable('-2 hour'));
178+
179+
$cacheStrategy->add($embeddedResponse);
180+
$cacheStrategy->update($mainResponse);
181+
182+
$this->assertFalse($mainResponse->headers->has('Last-Modified'));
157183
}
158184

159185
public function testMainResponseIsNotCacheableWhenEmbeddedResponseIsNotCacheable()

0 commit comments

Comments
 (0)