Skip to content

Commit cb810f4

Browse files
committed
Public responses without lifetime should not remove lifetime for the resulting response
1 parent d5fb833 commit cb810f4

File tree

2 files changed

+49
-6
lines changed

2 files changed

+49
-6
lines changed

HttpCache/ResponseCacheStrategy.php

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,15 @@ public function add(Response $response)
8181
return;
8282
}
8383

84+
$isHeuristicallyCacheable = $response->headers->hasCacheControlDirective('public');
8485
$maxAge = $response->headers->hasCacheControlDirective('max-age') ? (int) $response->headers->getCacheControlDirective('max-age') : null;
85-
$this->storeRelativeAgeDirective('max-age', $maxAge, $age);
86+
$this->storeRelativeAgeDirective('max-age', $maxAge, $age, $isHeuristicallyCacheable);
8687
$sharedMaxAge = $response->headers->hasCacheControlDirective('s-maxage') ? (int) $response->headers->getCacheControlDirective('s-maxage') : $maxAge;
87-
$this->storeRelativeAgeDirective('s-maxage', $sharedMaxAge, $age);
88+
$this->storeRelativeAgeDirective('s-maxage', $sharedMaxAge, $age, $isHeuristicallyCacheable);
8889

8990
$expires = $response->getExpires();
9091
$expires = null !== $expires ? (int) $expires->format('U') - (int) $response->getDate()->format('U') : null;
91-
$this->storeRelativeAgeDirective('expires', $expires >= 0 ? $expires : null, 0);
92+
$this->storeRelativeAgeDirective('expires', $expires >= 0 ? $expires : null, 0, $isHeuristicallyCacheable);
9293
}
9394

9495
/**
@@ -199,11 +200,29 @@ private function willMakeFinalResponseUncacheable(Response $response): bool
199200
* we have to subtract the age so that the value is normalized for an age of 0.
200201
*
201202
* If the value is lower than the currently stored value, we update the value, to keep a rolling
202-
* minimal value of each instruction. If the value is NULL, the directive will not be set on the final response.
203+
* minimal value of each instruction.
204+
*
205+
* If the value is NULL and the isHeuristicallyCacheable parameter is false, the directive will
206+
* not be set on the final response. In this case, not all responses had the directive set and no
207+
* value can be found that satisfies the requirements of all responses. The directive will be dropped
208+
* from the final response.
209+
*
210+
* If the isHeuristicallyCacheable parameter is true, however, the current response has been marked
211+
* as cacheable in a public (shared) cache, but did not provide an explicit lifetime that would serve
212+
* as an upper bound. In this case, we can proceed and possibly keep the directive on the final response.
203213
*/
204-
private function storeRelativeAgeDirective(string $directive, ?int $value, int $age)
214+
private function storeRelativeAgeDirective(string $directive, ?int $value, int $age, bool $isHeuristicallyCacheable)
205215
{
206216
if (null === $value) {
217+
if ($isHeuristicallyCacheable) {
218+
/*
219+
* See https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.2
220+
* This particular response does not require maximum lifetime; heuristics might be applied.
221+
* Other responses, however, might have more stringent requirements on maximum lifetime.
222+
* So, return early here so that the final response can have the more limiting value set.
223+
*/
224+
return;
225+
}
207226
$this->ageDirectives[$directive] = false;
208227
}
209228

Tests/HttpCache/ResponseCacheStrategyTest.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ public function cacheControlMergingProvider()
370370
];
371371

372372
yield 'merge max-age and s-maxage' => [
373-
['public' => true, 's-maxage' => '60', 'max-age' => null],
373+
['public' => true, 'max-age' => '60'],
374374
['public' => true, 's-maxage' => 3600],
375375
[
376376
['public' => true, 'max-age' => 60],
@@ -393,6 +393,30 @@ public function cacheControlMergingProvider()
393393
],
394394
];
395395

396+
yield 'public subresponse without lifetime does not remove lifetime for main response' => [
397+
['public' => true, 's-maxage' => '30', 'max-age' => null],
398+
['public' => true, 's-maxage' => '30'],
399+
[
400+
['public' => true],
401+
],
402+
];
403+
404+
yield 'lifetime for subresponse is kept when main response has no lifetime' => [
405+
['public' => true, 'max-age' => '30'],
406+
['public' => true],
407+
[
408+
['public' => true, 'max-age' => '30'],
409+
],
410+
];
411+
412+
yield 's-maxage on the subresponse implies public, so the result is public as well' => [
413+
['public' => true, 'max-age' => '10', 's-maxage' => null],
414+
['public' => true, 'max-age' => '10'],
415+
[
416+
['max-age' => '30', 's-maxage' => '20'],
417+
],
418+
];
419+
396420
yield 'result is private when combining private responses' => [
397421
['no-cache' => false, 'must-revalidate' => false, 'private' => true],
398422
['s-maxage' => 60, 'private' => true],

0 commit comments

Comments
 (0)