Skip to content

Commit d17d38d

Browse files
committed
bug #26643 Fix that ESI/SSI processing can turn a "private" response "public" (mpdude)
This PR was squashed before being merged into the 2.7 branch (closes #26643). Discussion ---------- Fix that ESI/SSI processing can turn a "private" response "public" | Q | A | ------------- | --- | Branch? | 2.7 | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | | License | MIT | Doc PR | Under the condition that * we are merging in at least one *embedded* response, * all *embedded* responses are `public`, * the *main* response is `private` and * all responses use expiration-based caching (note: no `s-maxage` on the *main* response) ... the resulting response will turn to `Cache-Control: public`. The real issue is that when all responses use expiration-based caching, a combined max age is computed. This is set on the *main* response using `Response::setSharedMaxAge()`, which implicitly sets `Cache-Control: public`. The fix provided in this PR solves the problem by applying the same logic to the *main* response that is applied for *embedded* responses, namely that responses with `!Response::isCacheable()` will make the resulting response have `Cache-Control: private, no-cache, must-revalidate` and have `(s)max-age` removed. This makes the change easy to understand, but makes responses uncacheable too often. This is because the `Response::isCacheable()` method was written to determine whether it is safe for a shared cache to keep the response, which is not the case as soon as a `private` response is involved. This might be improved upon in another PR. Commits ------- 3d27b59 Fix that ESI/SSI processing can turn a \"private\" response \"public\"
2 parents a3af3d3 + 3d27b59 commit d17d38d

File tree

3 files changed

+29
-5
lines changed

3 files changed

+29
-5
lines changed

src/Symfony/Component/HttpFoundation/Response.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -507,13 +507,19 @@ public function getCharset()
507507
}
508508

509509
/**
510-
* Returns true if the response is worth caching under any circumstance.
510+
* Returns true if the response may safely be kept in a shared (surrogate) cache.
511511
*
512512
* Responses marked "private" with an explicit Cache-Control directive are
513513
* considered uncacheable.
514514
*
515515
* Responses with neither a freshness lifetime (Expires, max-age) nor cache
516-
* validator (Last-Modified, ETag) are considered uncacheable.
516+
* validator (Last-Modified, ETag) are considered uncacheable because there is
517+
* no way to tell when or how to remove them from the cache.
518+
*
519+
* Note that RFC 7231 and RFC 7234 possibly allow for a more permissive implementation,
520+
* for example "status codes that are defined as cacheable by default [...]
521+
* can be reused by a cache with heuristic expiration unless otherwise indicated"
522+
* (https://tools.ietf.org/html/rfc7231#section-6.1)
517523
*
518524
* @return bool true if the response is worth caching, false otherwise
519525
*/

src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public function update(Response $response)
7272
$response->setLastModified(null);
7373
}
7474

75-
if (!$response->isFresh()) {
75+
if (!$response->isFresh() || !$response->isCacheable()) {
7676
$this->cacheable = false;
7777
}
7878

src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php

+20-2
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,26 @@ public function testEmbeddingPrivateResponseMakesMainResponsePrivate()
175175
$cacheStrategy->update($masterResponse);
176176

177177
$this->assertTrue($masterResponse->headers->hasCacheControlDirective('private'));
178-
// Not sure if we should pass "max-age: 60" in this case, as long as the response is private and
179-
// that's the more conservative of both the master and embedded response...?
178+
$this->assertFalse($masterResponse->headers->hasCacheControlDirective('public'));
179+
}
180+
181+
public function testEmbeddingPublicResponseDoesNotMakeMainResponsePublic()
182+
{
183+
$cacheStrategy = new ResponseCacheStrategy();
184+
185+
$masterResponse = new Response();
186+
$masterResponse->setPrivate(); // this is the default, but let's be explicit
187+
$masterResponse->setMaxAge(100);
188+
189+
$embeddedResponse = new Response();
190+
$embeddedResponse->setPublic();
191+
$embeddedResponse->setSharedMaxAge(100);
192+
193+
$cacheStrategy->add($embeddedResponse);
194+
$cacheStrategy->update($masterResponse);
195+
196+
$this->assertTrue($masterResponse->headers->hasCacheControlDirective('private'));
197+
$this->assertFalse($masterResponse->headers->hasCacheControlDirective('public'));
180198
}
181199

182200
public function testResponseIsExiprableWhenEmbeddedResponseCombinesExpiryAndValidation()

0 commit comments

Comments
 (0)