Skip to content

Commit 354bedd

Browse files
committed
Merge branch '6.4' into 7.1
* 6.4: [Form] Remove unnecessary imports minor #58472 CS: clean some whitespaces/indentation (keradus) Fix newline harden test to not depend on the system's configured default timezone [Form] Support intl.use_exceptions/error_level in NumberToLocalizedStringTransformer [Doctrine][Messenger] Use common sequence name to get id from Oracle [ExpressionLanguage] Add missing test case for `Lexer` [FrameworkBundle] Fix passing request_stack to session.listener ensure session storages are opened in tests before destroying them [Serializer] Fix `ObjectNormalizer` gives warnings on normalizing with public static property [HttpKernel] Correctly merge `max-age`/`s-maxage` and `Expires` headers [Security][Validator] Check translations for Czech [Security] Fix serialized object representation in tests [DoctrineBridge] Fix risky test warnings [Serializer] Catch `NotNormalizableValueException` for variadic parameters
2 parents 74b0d0e + d6662b1 commit 354bedd

File tree

2 files changed

+88
-32
lines changed

2 files changed

+88
-32
lines changed

HttpCache/ResponseCacheStrategy.php

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class ResponseCacheStrategy implements ResponseCacheStrategyInterface
5151
private array $ageDirectives = [
5252
'max-age' => null,
5353
's-maxage' => null,
54-
'expires' => null,
54+
'expires' => false,
5555
];
5656

5757
public function add(Response $response): void
@@ -79,15 +79,30 @@ public function add(Response $response): void
7979
return;
8080
}
8181

82-
$isHeuristicallyCacheable = $response->headers->hasCacheControlDirective('public');
8382
$maxAge = $response->headers->hasCacheControlDirective('max-age') ? (int) $response->headers->getCacheControlDirective('max-age') : null;
84-
$this->storeRelativeAgeDirective('max-age', $maxAge, $age, $isHeuristicallyCacheable);
8583
$sharedMaxAge = $response->headers->hasCacheControlDirective('s-maxage') ? (int) $response->headers->getCacheControlDirective('s-maxage') : $maxAge;
86-
$this->storeRelativeAgeDirective('s-maxage', $sharedMaxAge, $age, $isHeuristicallyCacheable);
87-
8884
$expires = $response->getExpires();
8985
$expires = null !== $expires ? (int) $expires->format('U') - (int) $response->getDate()->format('U') : null;
90-
$this->storeRelativeAgeDirective('expires', $expires >= 0 ? $expires : null, 0, $isHeuristicallyCacheable);
86+
87+
// See https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.2
88+
// If a response is "public" but does not have maximum lifetime, heuristics might be applied.
89+
// Do not store NULL values so the final response can have more limiting value from other responses.
90+
$isHeuristicallyCacheable = $response->headers->hasCacheControlDirective('public')
91+
&& null === $maxAge
92+
&& null === $sharedMaxAge
93+
&& null === $expires;
94+
95+
if (!$isHeuristicallyCacheable || null !== $maxAge || null !== $expires) {
96+
$this->storeRelativeAgeDirective('max-age', $maxAge, $expires, $age);
97+
}
98+
99+
if (!$isHeuristicallyCacheable || null !== $sharedMaxAge || null !== $expires) {
100+
$this->storeRelativeAgeDirective('s-maxage', $sharedMaxAge, $expires, $age);
101+
}
102+
103+
if (null !== $expires) {
104+
$this->ageDirectives['expires'] = true;
105+
}
91106

92107
if (false !== $this->lastModified) {
93108
$lastModified = $response->getLastModified();
@@ -146,9 +161,9 @@ public function update(Response $response): void
146161
}
147162
}
148163

149-
if (is_numeric($this->ageDirectives['expires'])) {
164+
if ($this->ageDirectives['expires'] && null !== $maxAge) {
150165
$date = clone $response->getDate();
151-
$date = $date->modify('+'.($this->ageDirectives['expires'] + $this->age).' seconds');
166+
$date = $date->modify('+'.$maxAge.' seconds');
152167
$response->setExpires($date);
153168
}
154169
}
@@ -198,33 +213,16 @@ private function willMakeFinalResponseUncacheable(Response $response): bool
198213
* we have to subtract the age so that the value is normalized for an age of 0.
199214
*
200215
* If the value is lower than the currently stored value, we update the value, to keep a rolling
201-
* minimal value of each instruction.
202-
*
203-
* If the value is NULL and the isHeuristicallyCacheable parameter is false, the directive will
204-
* not be set on the final response. In this case, not all responses had the directive set and no
205-
* value can be found that satisfies the requirements of all responses. The directive will be dropped
206-
* from the final response.
207-
*
208-
* If the isHeuristicallyCacheable parameter is true, however, the current response has been marked
209-
* as cacheable in a public (shared) cache, but did not provide an explicit lifetime that would serve
210-
* as an upper bound. In this case, we can proceed and possibly keep the directive on the final response.
216+
* minimal value of each instruction. If the value is NULL, the directive will not be set on the final response.
211217
*/
212-
private function storeRelativeAgeDirective(string $directive, ?int $value, int $age, bool $isHeuristicallyCacheable): void
218+
private function storeRelativeAgeDirective(string $directive, ?int $value, ?int $expires, int $age): void
213219
{
214-
if (null === $value) {
215-
if ($isHeuristicallyCacheable) {
216-
/*
217-
* See https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.2
218-
* This particular response does not require maximum lifetime; heuristics might be applied.
219-
* Other responses, however, might have more stringent requirements on maximum lifetime.
220-
* So, return early here so that the final response can have the more limiting value set.
221-
*/
222-
return;
223-
}
220+
if (null === $value && null === $expires) {
224221
$this->ageDirectives[$directive] = false;
225222
}
226223

227224
if (false !== $this->ageDirectives[$directive]) {
225+
$value = min($value ?? PHP_INT_MAX, $expires ?? PHP_INT_MAX);
228226
$value -= $age;
229227
$this->ageDirectives[$directive] = null !== $this->ageDirectives[$directive] ? min($this->ageDirectives[$directive], $value) : $value;
230228
}

Tests/HttpCache/ResponseCacheStrategyTest.php

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,64 @@ public function testSharedMaxAgeNotSetIfNotSetInMainRequest()
7272
$this->assertFalse($response->headers->hasCacheControlDirective('s-maxage'));
7373
}
7474

75+
public function testExpiresHeaderUpdatedFromMaxAge()
76+
{
77+
$cacheStrategy = new ResponseCacheStrategy();
78+
79+
$response1 = new Response();
80+
$response1->setExpires(new \DateTime('+ 1 hour'));
81+
$response1->setPublic();
82+
$cacheStrategy->add($response1);
83+
84+
$response = new Response();
85+
$response->setMaxAge(0);
86+
$response->setSharedMaxAge(86400);
87+
$cacheStrategy->update($response);
88+
89+
$this->assertSame('0', $response->headers->getCacheControlDirective('max-age'));
90+
$this->assertSame('3600', $response->headers->getCacheControlDirective('s-maxage'));
91+
92+
// Expires header must be same as Date header because "max-age" is 0.
93+
$this->assertSame($response->headers->get('Date'), $response->headers->get('Expires'));
94+
}
95+
96+
public function testMaxAgeUpdatedFromExpiresHeader()
97+
{
98+
$cacheStrategy = new ResponseCacheStrategy();
99+
100+
$response1 = new Response();
101+
$response1->setExpires(new \DateTime('+ 1 hour', new \DateTimeZone('UTC')));
102+
$response1->setPublic();
103+
$cacheStrategy->add($response1);
104+
105+
$response = new Response();
106+
$response->setMaxAge(86400);
107+
$cacheStrategy->update($response);
108+
109+
$this->assertSame('3600', $response->headers->getCacheControlDirective('max-age'));
110+
$this->assertNull($response->headers->getCacheControlDirective('s-maxage'));
111+
$this->assertSame((new \DateTime('+ 1 hour', new \DateTimeZone('UTC')))->format('D, d M Y H:i:s').' GMT', $response->headers->get('Expires'));
112+
}
113+
114+
public function testMaxAgeAndSharedMaxAgeUpdatedFromExpiresHeader()
115+
{
116+
$cacheStrategy = new ResponseCacheStrategy();
117+
118+
$response1 = new Response();
119+
$response1->setExpires(new \DateTime('+ 1 day', new \DateTimeZone('UTC')));
120+
$response1->setPublic();
121+
$cacheStrategy->add($response1);
122+
123+
$response = new Response();
124+
$response->setMaxAge(3600);
125+
$response->setSharedMaxAge(86400);
126+
$cacheStrategy->update($response);
127+
128+
$this->assertSame('3600', $response->headers->getCacheControlDirective('max-age'));
129+
$this->assertSame('86400', $response->headers->getCacheControlDirective('s-maxage'));
130+
$this->assertSame((new \DateTime('+ 1 hour', new \DateTimeZone('UTC')))->format('D, d M Y H:i:s').' GMT', $response->headers->get('Expires'));
131+
}
132+
75133
public function testMainResponseNotCacheableWhenEmbeddedResponseRequiresValidation()
76134
{
77135
$cacheStrategy = new ResponseCacheStrategy();
@@ -287,7 +345,7 @@ public function testResponseIsExpirableButNotValidateableWhenMainResponseCombine
287345
*
288346
* @dataProvider cacheControlMergingProvider
289347
*/
290-
public function testCacheControlMerging(array $expects, array $master, array $surrogates)
348+
public function testCacheControlMerging(array $expects, array $main, array $surrogates)
291349
{
292350
$cacheStrategy = new ResponseCacheStrategy();
293351
$buildResponse = function ($config) {
@@ -333,7 +391,7 @@ public function testCacheControlMerging(array $expects, array $master, array $su
333391
$cacheStrategy->add($buildResponse($config));
334392
}
335393

336-
$response = $buildResponse($master);
394+
$response = $buildResponse($main);
337395
$cacheStrategy->update($response);
338396

339397
foreach ($expects as $key => $value) {
@@ -415,7 +473,7 @@ public static function cacheControlMergingProvider()
415473
];
416474

417475
yield 'merge max-age and s-maxage' => [
418-
['public' => true, 'max-age' => '60'],
476+
['public' => true, 'max-age' => null, 's-maxage' => '60'],
419477
['public' => true, 's-maxage' => 3600],
420478
[
421479
['public' => true, 'max-age' => 60],

0 commit comments

Comments
 (0)