diff --git a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php index 5f372c566dd44..7185c9105be6f 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php @@ -50,7 +50,7 @@ class ResponseCacheStrategy implements ResponseCacheStrategyInterface private $ageDirectives = [ 'max-age' => null, 's-maxage' => null, - 'expires' => null, + 'expires' => false, ]; /** @@ -81,15 +81,30 @@ public function add(Response $response) return; } - $isHeuristicallyCacheable = $response->headers->hasCacheControlDirective('public'); $maxAge = $response->headers->hasCacheControlDirective('max-age') ? (int) $response->headers->getCacheControlDirective('max-age') : null; - $this->storeRelativeAgeDirective('max-age', $maxAge, $age, $isHeuristicallyCacheable); $sharedMaxAge = $response->headers->hasCacheControlDirective('s-maxage') ? (int) $response->headers->getCacheControlDirective('s-maxage') : $maxAge; - $this->storeRelativeAgeDirective('s-maxage', $sharedMaxAge, $age, $isHeuristicallyCacheable); - $expires = $response->getExpires(); $expires = null !== $expires ? (int) $expires->format('U') - (int) $response->getDate()->format('U') : null; - $this->storeRelativeAgeDirective('expires', $expires >= 0 ? $expires : null, 0, $isHeuristicallyCacheable); + + // See https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.2 + // If a response is "public" but does not have maximum lifetime, heuristics might be applied. + // Do not store NULL values so the final response can have more limiting value from other responses. + $isHeuristicallyCacheable = $response->headers->hasCacheControlDirective('public') + && null === $maxAge + && null === $sharedMaxAge + && null === $expires; + + if (!$isHeuristicallyCacheable || null !== $maxAge || null !== $expires) { + $this->storeRelativeAgeDirective('max-age', $maxAge, $expires, $age); + } + + if (!$isHeuristicallyCacheable || null !== $sharedMaxAge || null !== $expires) { + $this->storeRelativeAgeDirective('s-maxage', $sharedMaxAge, $expires, $age); + } + + if (null !== $expires) { + $this->ageDirectives['expires'] = true; + } } /** @@ -102,7 +117,7 @@ public function update(Response $response) return; } - // Remove validation related headers of the master response, + // Remove validation related headers of the final response, // because some of the response content comes from at least // one embedded response (which likely has a different caching strategy). $response->setEtag(null); @@ -145,9 +160,9 @@ public function update(Response $response) } } - if (is_numeric($this->ageDirectives['expires'])) { + if ($this->ageDirectives['expires'] && null !== $maxAge) { $date = clone $response->getDate(); - $date = $date->modify('+'.($this->ageDirectives['expires'] + $this->age).' seconds'); + $date = $date->modify('+'.$maxAge.' seconds'); $response->setExpires($date); } } @@ -200,33 +215,16 @@ private function willMakeFinalResponseUncacheable(Response $response): bool * we have to subtract the age so that the value is normalized for an age of 0. * * If the value is lower than the currently stored value, we update the value, to keep a rolling - * minimal value of each instruction. - * - * If the value is NULL and the isHeuristicallyCacheable parameter is false, the directive will - * not be set on the final response. In this case, not all responses had the directive set and no - * value can be found that satisfies the requirements of all responses. The directive will be dropped - * from the final response. - * - * If the isHeuristicallyCacheable parameter is true, however, the current response has been marked - * as cacheable in a public (shared) cache, but did not provide an explicit lifetime that would serve - * as an upper bound. In this case, we can proceed and possibly keep the directive on the final response. + * minimal value of each instruction. If the value is NULL, the directive will not be set on the final response. */ - private function storeRelativeAgeDirective(string $directive, ?int $value, int $age, bool $isHeuristicallyCacheable) + private function storeRelativeAgeDirective(string $directive, ?int $value, ?int $expires, int $age): void { - if (null === $value) { - if ($isHeuristicallyCacheable) { - /* - * See https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.2 - * This particular response does not require maximum lifetime; heuristics might be applied. - * Other responses, however, might have more stringent requirements on maximum lifetime. - * So, return early here so that the final response can have the more limiting value set. - */ - return; - } + if (null === $value && null === $expires) { $this->ageDirectives[$directive] = false; } if (false !== $this->ageDirectives[$directive]) { + $value = min($value ?? PHP_INT_MAX, $expires ?? PHP_INT_MAX); $value -= $age; $this->ageDirectives[$directive] = null !== $this->ageDirectives[$directive] ? min($this->ageDirectives[$directive], $value) : $value; } diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php index ce9f5ba1a158a..8a4cce53bffa8 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php @@ -76,6 +76,64 @@ public function testSharedMaxAgeNotSetIfNotSetInMainRequest() $this->assertFalse($response->headers->hasCacheControlDirective('s-maxage')); } + public function testExpiresHeaderUpdatedFromMaxAge() + { + $cacheStrategy = new ResponseCacheStrategy(); + + $response1 = new Response(); + $response1->setExpires(new \DateTime('+ 1 hour')); + $response1->setPublic(); + $cacheStrategy->add($response1); + + $response = new Response(); + $response->setMaxAge(0); + $response->setSharedMaxAge(86400); + $cacheStrategy->update($response); + + $this->assertSame('0', $response->headers->getCacheControlDirective('max-age')); + $this->assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); + + // Expires header must be same as Date header because "max-age" is 0. + $this->assertSame($response->headers->get('Date'), $response->headers->get('Expires')); + } + + public function testMaxAgeUpdatedFromExpiresHeader() + { + $cacheStrategy = new ResponseCacheStrategy(); + + $response1 = new Response(); + $response1->setExpires(new \DateTime('+ 1 hour')); + $response1->setPublic(); + $cacheStrategy->add($response1); + + $response = new Response(); + $response->setMaxAge(86400); + $cacheStrategy->update($response); + + $this->assertSame('3600', $response->headers->getCacheControlDirective('max-age')); + $this->assertNull($response->headers->getCacheControlDirective('s-maxage')); + $this->assertSame((new \DateTime('+ 1 hour'))->format('D, d M Y H:i:s').' GMT', $response->headers->get('Expires')); + } + + public function testMaxAgeAndSharedMaxAgeUpdatedFromExpiresHeader() + { + $cacheStrategy = new ResponseCacheStrategy(); + + $response1 = new Response(); + $response1->setExpires(new \DateTime('+ 1 day')); + $response1->setPublic(); + $cacheStrategy->add($response1); + + $response = new Response(); + $response->setMaxAge(3600); + $response->setSharedMaxAge(86400); + $cacheStrategy->update($response); + + $this->assertSame('3600', $response->headers->getCacheControlDirective('max-age')); + $this->assertSame('86400', $response->headers->getCacheControlDirective('s-maxage')); + $this->assertSame((new \DateTime('+ 1 hour'))->format('D, d M Y H:i:s').' GMT', $response->headers->get('Expires')); + } + public function testMainResponseNotCacheableWhenEmbeddedResponseRequiresValidation() { $cacheStrategy = new ResponseCacheStrategy(); @@ -243,7 +301,7 @@ public function testResponseIsExpirableButNotValidateableWhenMainResponseCombine * * @dataProvider cacheControlMergingProvider */ - public function testCacheControlMerging(array $expects, array $master, array $surrogates) + public function testCacheControlMerging(array $expects, array $main, array $surrogates) { $cacheStrategy = new ResponseCacheStrategy(); $buildResponse = function ($config) { @@ -289,7 +347,7 @@ public function testCacheControlMerging(array $expects, array $master, array $su $cacheStrategy->add($buildResponse($config)); } - $response = $buildResponse($master); + $response = $buildResponse($main); $cacheStrategy->update($response); foreach ($expects as $key => $value) { @@ -371,7 +429,7 @@ public static function cacheControlMergingProvider() ]; yield 'merge max-age and s-maxage' => [ - ['public' => true, 'max-age' => '60'], + ['public' => true, 'max-age' => null, 's-maxage' => '60'], ['public' => true, 's-maxage' => 3600], [ ['public' => true, 'max-age' => 60], pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy