@@ -1522,6 +1522,168 @@ public function testUsesOriginalRequestForSurrogate()
1522
1522
$ cache ->handle ($ request , HttpKernelInterface::SUB_REQUEST );
1523
1523
}
1524
1524
1525
+ public function testStaleIfErrorMustNotResetLifetime ()
1526
+ {
1527
+ // Make sure we don't accidentally treat the response as fresh (revalidated) again
1528
+ // when stale-if-error handling kicks in.
1529
+
1530
+ $ responses = [
1531
+ [
1532
+ 'status ' => 200 ,
1533
+ 'body ' => 'OK ' ,
1534
+ // This is cacheable and can be used in stale-if-error cases:
1535
+ 'headers ' => ['Cache-Control ' => 'public, max-age=10 ' , 'ETag ' => 'some-etag ' ],
1536
+ ],
1537
+ [
1538
+ 'status ' => 500 ,
1539
+ 'body ' => 'FAIL ' ,
1540
+ 'headers ' => [],
1541
+ ],
1542
+ [
1543
+ 'status ' => 500 ,
1544
+ 'body ' => 'FAIL ' ,
1545
+ 'headers ' => [],
1546
+ ],
1547
+ ];
1548
+
1549
+ $ this ->setNextResponses ($ responses );
1550
+ $ this ->cacheConfig ['stale_if_error ' ] = 10 ;
1551
+
1552
+ $ this ->request ('GET ' , '/ ' ); // warm cache
1553
+
1554
+ sleep (15 ); // now the entry is stale, but still within the grace period (10s max-age + 10s stale-if-error)
1555
+
1556
+ $ this ->request ('GET ' , '/ ' ); // hit backend error
1557
+ $ this ->assertEquals (200 , $ this ->response ->getStatusCode ()); // stale-if-error saved the day
1558
+ $ this ->assertEquals (15 , $ this ->response ->getAge ());
1559
+
1560
+ sleep (10 ); // now we're outside the grace period
1561
+
1562
+ $ this ->request ('GET ' , '/ ' ); // hit backend error
1563
+ $ this ->assertEquals (500 , $ this ->response ->getStatusCode ()); // fail
1564
+ }
1565
+
1566
+ /**
1567
+ * @dataProvider getResponseDataThatMayBeServedStaleIfError
1568
+ */
1569
+ public function testResponsesThatMayBeUsedStaleIfError ($ responseHeaders , $ sleepBetweenRequests = null )
1570
+ {
1571
+ $ responses = [
1572
+ [
1573
+ 'status ' => 200 ,
1574
+ 'body ' => 'OK ' ,
1575
+ 'headers ' => $ responseHeaders ,
1576
+ ],
1577
+ [
1578
+ 'status ' => 500 ,
1579
+ 'body ' => 'FAIL ' ,
1580
+ 'headers ' => [],
1581
+ ],
1582
+ ];
1583
+
1584
+ $ this ->setNextResponses ($ responses );
1585
+ $ this ->cacheConfig ['stale_if_error ' ] = 10 ; // after stale, may be served for 10s
1586
+
1587
+ $ this ->request ('GET ' , '/ ' ); // warm cache
1588
+
1589
+ if ($ sleepBetweenRequests ) {
1590
+ sleep ($ sleepBetweenRequests );
1591
+ }
1592
+
1593
+ $ this ->request ('GET ' , '/ ' ); // hit backend error
1594
+
1595
+ $ this ->assertEquals (200 , $ this ->response ->getStatusCode ());
1596
+ $ this ->assertEquals ('OK ' , $ this ->response ->getContent ());
1597
+ $ this ->assertTraceContains ('stale-if-error ' );
1598
+ }
1599
+
1600
+ public function getResponseDataThatMayBeServedStaleIfError ()
1601
+ {
1602
+ // All data sets assume that a 10s stale-if-error grace period has been configured
1603
+ yield 'public, max-age expired ' => [['Cache-Control ' => 'public, max-age=60 ' ], 65 ];
1604
+ yield 'public, validateable with ETag, no TTL ' => [['Cache-Control ' => 'public ' , 'ETag ' => 'some-etag ' ], 5 ];
1605
+ yield 'public, validateable with Last-Modified, no TTL ' => [['Cache-Control ' => 'public ' , 'Last-Modified ' => 'yesterday ' ], 5 ];
1606
+ yield 'public, s-maxage will be served stale-if-error, even if the RFC mandates otherwise ' => [['Cache-Control ' => 'public, s-maxage=20 ' ], 25 ];
1607
+ }
1608
+
1609
+ /**
1610
+ * @dataProvider getResponseDataThatMustNotBeServedStaleIfError
1611
+ */
1612
+ public function testResponsesThatMustNotBeUsedStaleIfError ($ responseHeaders , $ sleepBetweenRequests = null )
1613
+ {
1614
+ $ responses = [
1615
+ [
1616
+ 'status ' => 200 ,
1617
+ 'body ' => 'OK ' ,
1618
+ 'headers ' => $ responseHeaders ,
1619
+ ],
1620
+ [
1621
+ 'status ' => 500 ,
1622
+ 'body ' => 'FAIL ' ,
1623
+ 'headers ' => [],
1624
+ ],
1625
+ ];
1626
+
1627
+ $ this ->setNextResponses ($ responses );
1628
+ $ this ->cacheConfig ['stale_if_error ' ] = 10 ; // after stale, may be served for 10s
1629
+ $ this ->cacheConfig ['strict_smaxage ' ] = true ; // full RFC compliance for this feature
1630
+
1631
+ $ this ->request ('GET ' , '/ ' ); // warm cache
1632
+
1633
+ if ($ sleepBetweenRequests ) {
1634
+ sleep ($ sleepBetweenRequests );
1635
+ }
1636
+
1637
+ $ this ->request ('GET ' , '/ ' ); // hit backend error
1638
+
1639
+ $ this ->assertEquals (500 , $ this ->response ->getStatusCode ());
1640
+ }
1641
+
1642
+ public function getResponseDataThatMustNotBeServedStaleIfError ()
1643
+ {
1644
+ // All data sets assume that a 10s stale-if-error grace period has been configured
1645
+ yield 'public, no TTL but beyond grace period ' => [['Cache-Control ' => 'public ' ], 15 ];
1646
+ yield 'public, validateable with ETag, no TTL but beyond grace period ' => [['Cache-Control ' => 'public ' , 'ETag ' => 'some-etag ' ], 15 ];
1647
+ yield 'public, validateable with Last-Modified, no TTL but beyond grace period ' => [['Cache-Control ' => 'public ' , 'Last-Modified ' => 'yesterday ' ], 15 ];
1648
+ yield 'public, stale beyond grace period ' => [['Cache-Control ' => 'public, max-age=10 ' ], 30 ];
1649
+
1650
+ // Cache-control values that prohibit serving stale responses or responses without positive validation -
1651
+ // see https://tools.ietf.org/html/rfc7234#section-4.2.4 and
1652
+ // https://tools.ietf.org/html/rfc7234#section-5.2.2
1653
+ yield 'no-cache requires positive validation ' => [['Cache-Control ' => 'public, no-cache ' , 'ETag ' => 'some-etag ' ]];
1654
+ yield 'no-cache requires positive validation, even if fresh ' => [['Cache-Control ' => 'public, no-cache, max-age=10 ' ]];
1655
+ yield 'must-revalidate requires positive validation once stale ' => [['Cache-Control ' => 'public, max-age=10, must-revalidate ' ], 15 ];
1656
+ yield 'proxy-revalidate requires positive validation once stale ' => [['Cache-Control ' => 'public, max-age=10, proxy-revalidate ' ], 15 ];
1657
+ }
1658
+
1659
+ public function testStaleIfErrorWhenStrictSmaxageDisabled ()
1660
+ {
1661
+ $ responses = [
1662
+ [
1663
+ 'status ' => 200 ,
1664
+ 'body ' => 'OK ' ,
1665
+ 'headers ' => ['Cache-Control ' => 'public, s-maxage=20 ' ],
1666
+ ],
1667
+ [
1668
+ 'status ' => 500 ,
1669
+ 'body ' => 'FAIL ' ,
1670
+ 'headers ' => [],
1671
+ ],
1672
+ ];
1673
+
1674
+ $ this ->setNextResponses ($ responses );
1675
+ $ this ->cacheConfig ['stale_if_error ' ] = 10 ;
1676
+ $ this ->cacheConfig ['strict_smaxage ' ] = false ;
1677
+
1678
+ $ this ->request ('GET ' , '/ ' ); // warm cache
1679
+ sleep (25 );
1680
+ $ this ->request ('GET ' , '/ ' ); // hit backend error
1681
+
1682
+ $ this ->assertEquals (200 , $ this ->response ->getStatusCode ());
1683
+ $ this ->assertEquals ('OK ' , $ this ->response ->getContent ());
1684
+ $ this ->assertTraceContains ('stale-if-error ' );
1685
+ }
1686
+
1525
1687
public function testTraceHeaderNameCanBeChanged ()
1526
1688
{
1527
1689
$ this ->cacheConfig ['trace_header ' ] = 'X-My-Header ' ;
0 commit comments