@@ -1329,6 +1329,175 @@ public function testEsiCacheSendsTheLowestTtlForHeadRequests()
13291329$ this ->assertEquals (100 ,$ this ->response ->getTtl ());
13301330 }
13311331
1332+ public function testEsiCacheIncludesEmbeddedResponseContentWhenMainResponseFailsRevalidationAndEmbeddedResponseIsFresh ()
1333+ {
1334+ $ this ->setNextResponses ([
1335+ [
1336+ 'status ' =>200 ,
1337+ 'body ' =>'main <esi:include src="/foo" /> ' ,
1338+ 'headers ' => [
1339+ 'Cache-Control ' =>'s-maxage=0 ' ,// goes stale immediately
1340+ 'Surrogate-Control ' =>'content="ESI/1.0" ' ,
1341+ 'Last-Modified ' =>'Mon, 12 Aug 2024 10:00:00 +0000 ' ,
1342+ ],
1343+ ],
1344+ [
1345+ 'status ' =>200 ,
1346+ 'body ' =>'embedded ' ,
1347+ 'headers ' => [
1348+ 'Cache-Control ' =>'s-maxage=10 ' ,// stays fresh
1349+ 'Last-Modified ' =>'Mon, 12 Aug 2024 10:05:00 +0000 ' ,
1350+ ]
1351+ ],
1352+ ]);
1353+
1354+ // prime the cache
1355+ $ this ->request ('GET ' ,'/ ' , [], [],true );
1356+ $ this ->assertSame (200 ,$ this ->response ->getStatusCode ());
1357+ $ this ->assertSame ('main embedded ' ,$ this ->response ->getContent ());
1358+ $ this ->assertSame ('Mon, 12 Aug 2024 10:05:00 +0000 ' ,$ this ->response ->getLastModified ()->format (\DATE_RFC2822 ));// max of both values
1359+
1360+ $ this ->setNextResponses ([
1361+ [
1362+ // On the next request, the main response has an updated Last-Modified (main page was modified)...
1363+ 'status ' =>200 ,
1364+ 'body ' =>'main <esi:include src="/foo" /> ' ,
1365+ 'headers ' => [
1366+ 'Cache-Control ' =>'s-maxage=0 ' ,
1367+ 'Surrogate-Control ' =>'content="ESI/1.0" ' ,
1368+ 'Last-Modified ' =>'Mon, 12 Aug 2024 10:10:00 +0000 ' ,
1369+ ],
1370+ ],
1371+ // no revalidation request happens for the embedded response, since it is still fresh
1372+ ]);
1373+
1374+ // Re-request with Last-Modified time that we received when the cache was primed
1375+ $ this ->request ('GET ' ,'/ ' , ['HTTP_IF_MODIFIED_SINCE ' =>'Mon, 12 Aug 2024 10:05:00 +0000 ' ], [],true );
1376+
1377+ $ this ->assertSame (200 ,$ this ->response ->getStatusCode ());
1378+
1379+ // The cache should use the content ("embedded") from the cached entry
1380+ $ this ->assertSame ('main embedded ' ,$ this ->response ->getContent ());
1381+
1382+ $ traces =$ this ->cache ->getTraces ();
1383+ $ this ->assertSame (['stale ' ,'invalid ' ,'store ' ],$ traces ['GET / ' ]);
1384+
1385+ // The embedded resource was still fresh
1386+ $ this ->assertSame (['fresh ' ],$ traces ['GET /foo ' ]);
1387+ }
1388+
1389+ public function testEsiCacheIncludesEmbeddedResponseContentWhenMainResponseFailsRevalidationAndEmbeddedResponseIsValid ()
1390+ {
1391+ $ this ->setNextResponses ([
1392+ [
1393+ 'status ' =>200 ,
1394+ 'body ' =>'main <esi:include src="/foo" /> ' ,
1395+ 'headers ' => [
1396+ 'Cache-Control ' =>'s-maxage=0 ' ,// goes stale immediately
1397+ 'Surrogate-Control ' =>'content="ESI/1.0" ' ,
1398+ 'Last-Modified ' =>'Mon, 12 Aug 2024 10:00:00 +0000 ' ,
1399+ ],
1400+ ],
1401+ [
1402+ 'status ' =>200 ,
1403+ 'body ' =>'embedded ' ,
1404+ 'headers ' => [
1405+ 'Cache-Control ' =>'s-maxage=0 ' ,// goes stale immediately
1406+ 'Last-Modified ' =>'Mon, 12 Aug 2024 10:05:00 +0000 ' ,
1407+ ]
1408+ ],
1409+ ]);
1410+
1411+ // prime the cache
1412+ $ this ->request ('GET ' ,'/ ' , [], [],true );
1413+ $ this ->assertSame (200 ,$ this ->response ->getStatusCode ());
1414+ $ this ->assertSame ('main embedded ' ,$ this ->response ->getContent ());
1415+ $ this ->assertSame ('Mon, 12 Aug 2024 10:05:00 +0000 ' ,$ this ->response ->getLastModified ()->format (\DATE_RFC2822 ));// max of both values
1416+
1417+ $ this ->setNextResponses ([
1418+ [
1419+ // On the next request, the main response has an updated Last-Modified (main page was modified)...
1420+ 'status ' =>200 ,
1421+ 'body ' =>'main <esi:include src="/foo" /> ' ,
1422+ 'headers ' => [
1423+ 'Cache-Control ' =>'s-maxage=0 ' ,
1424+ 'Surrogate-Control ' =>'content="ESI/1.0" ' ,
1425+ 'Last-Modified ' =>'Mon, 12 Aug 2024 10:10:00 +0000 ' ,
1426+ ],
1427+ ],
1428+ [
1429+ // We have a stale cache entry for the embedded response which will be revalidated.
1430+ // Let's assume the resource did not change, so the controller sends a 304 without content body.
1431+ 'status ' =>304 ,
1432+ 'body ' =>'' ,
1433+ 'headers ' => [
1434+ 'Cache-Control ' =>'s-maxage=0 ' ,
1435+ ],
1436+ ],
1437+ ]);
1438+
1439+ // Re-request with Last-Modified time that we received when the cache was primed
1440+ $ this ->request ('GET ' ,'/ ' , ['HTTP_IF_MODIFIED_SINCE ' =>'Mon, 12 Aug 2024 10:05:00 +0000 ' ], [],true );
1441+
1442+ $ this ->assertSame (200 ,$ this ->response ->getStatusCode ());
1443+
1444+ // The cache should use the content ("embedded") from the cached entry
1445+ $ this ->assertSame ('main embedded ' ,$ this ->response ->getContent ());
1446+
1447+ $ traces =$ this ->cache ->getTraces ();
1448+ $ this ->assertSame (['stale ' ,'invalid ' ,'store ' ],$ traces ['GET / ' ]);
1449+
1450+ // Check that the embedded resource was successfully revalidated
1451+ $ this ->assertSame (['stale ' ,'valid ' ,'store ' ],$ traces ['GET /foo ' ]);
1452+ }
1453+
1454+ public function testEsiCacheIncludesEmbeddedResponseContentWhenMainAndEmbeddedResponseAreFresh ()
1455+ {
1456+ $ this ->setNextResponses ([
1457+ [
1458+ 'status ' =>200 ,
1459+ 'body ' =>'main <esi:include src="/foo" /> ' ,
1460+ 'headers ' => [
1461+ 'Cache-Control ' =>'s-maxage=10 ' ,
1462+ 'Surrogate-Control ' =>'content="ESI/1.0" ' ,
1463+ 'Last-Modified ' =>'Mon, 12 Aug 2024 10:05:00 +0000 ' ,
1464+ ],
1465+ ],
1466+ [
1467+ 'status ' =>200 ,
1468+ 'body ' =>'embedded ' ,
1469+ 'headers ' => [
1470+ 'Cache-Control ' =>'s-maxage=10 ' ,
1471+ 'Last-Modified ' =>'Mon, 12 Aug 2024 10:00:00 +0000 ' ,
1472+ ]
1473+ ],
1474+ ]);
1475+
1476+ // prime the cache
1477+ $ this ->request ('GET ' ,'/ ' , [], [],true );
1478+ $ this ->assertSame (200 ,$ this ->response ->getStatusCode ());
1479+ $ this ->assertSame ('main embedded ' ,$ this ->response ->getContent ());
1480+ $ this ->assertSame ('Mon, 12 Aug 2024 10:05:00 +0000 ' ,$ this ->response ->getLastModified ()->format (\DATE_RFC2822 ));
1481+
1482+ // Assume that a client received 'Mon, 12 Aug 2024 10:00:00 +0000' as last-modified information in the past. This may, for example,
1483+ // be the case when the "main" response at that point had an older Last-Modified time, so the embedded response's Last-Modified time
1484+ // governed the result for the combined response. In other words, the client received a Last-Modified time that still validates the
1485+ // embedded response as of now, but no longer matches the Last-Modified time of the "main" resource.
1486+ // Now this client does a revalidation request.
1487+ $ this ->request ('GET ' ,'/ ' , ['HTTP_IF_MODIFIED_SINCE ' =>'Mon, 12 Aug 2024 10:00:00 +0000 ' ], [],true );
1488+
1489+ $ this ->assertSame (200 ,$ this ->response ->getStatusCode ());
1490+
1491+ // The cache should use the content ("embedded") from the cached entry
1492+ $ this ->assertSame ('main embedded ' ,$ this ->response ->getContent ());
1493+
1494+ $ traces =$ this ->cache ->getTraces ();
1495+ $ this ->assertSame (['fresh ' ],$ traces ['GET / ' ]);
1496+
1497+ // Check that the embedded resource was successfully revalidated
1498+ $ this ->assertSame (['fresh ' ],$ traces ['GET /foo ' ]);
1499+ }
1500+
13321501public function testEsiCacheForceValidation ()
13331502 {
13341503$ responses = [