Skip to content

[HttpKernel] ESI fragment content may be missing in conditional requests #58015

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,9 @@ public function handle(Request $request, int $type = HttpKernelInterface::MAIN_R

$response->prepare($request);

$response->isNotModified($request);
if (HttpKernelInterface::MAIN_REQUEST === $type) {
$response->isNotModified($request);
}

return $response;
}
Expand Down
169 changes: 169 additions & 0 deletions src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,175 @@ public function testEsiCacheSendsTheLowestTtlForHeadRequests()
$this->assertEquals(100, $this->response->getTtl());
}

public function testEsiCacheIncludesEmbeddedResponseContentWhenMainResponseFailsRevalidationAndEmbeddedResponseIsFresh()
{
$this->setNextResponses([
[
'status' => 200,
'body' => 'main <esi:include src="/foo" />',
'headers' => [
'Cache-Control' => 's-maxage=0', // goes stale immediately
'Surrogate-Control' => 'content="ESI/1.0"',
'Last-Modified' => 'Mon, 12 Aug 2024 10:00:00 +0000',
],
],
[
'status' => 200,
'body' => 'embedded',
'headers' => [
'Cache-Control' => 's-maxage=10', // stays fresh
'Last-Modified' => 'Mon, 12 Aug 2024 10:05:00 +0000',
]
],
]);

// prime the cache
$this->request('GET', '/', [], [], true);
$this->assertSame(200, $this->response->getStatusCode());
$this->assertSame('main embedded', $this->response->getContent());
$this->assertSame('Mon, 12 Aug 2024 10:05:00 +0000', $this->response->getLastModified()->format(\DATE_RFC2822)); // max of both values

$this->setNextResponses([
[
// On the next request, the main response has an updated Last-Modified (main page was modified)...
'status' => 200,
'body' => 'main <esi:include src="/foo" />',
'headers' => [
'Cache-Control' => 's-maxage=0',
'Surrogate-Control' => 'content="ESI/1.0"',
'Last-Modified' => 'Mon, 12 Aug 2024 10:10:00 +0000',
],
],
// no revalidation request happens for the embedded response, since it is still fresh
]);

// Re-request with Last-Modified time that we received when the cache was primed
$this->request('GET', '/', ['HTTP_IF_MODIFIED_SINCE' => 'Mon, 12 Aug 2024 10:05:00 +0000'], [], true);

$this->assertSame(200, $this->response->getStatusCode());

// The cache should use the content ("embedded") from the cached entry
$this->assertSame('main embedded', $this->response->getContent());

$traces = $this->cache->getTraces();
$this->assertSame(['stale', 'invalid', 'store'], $traces['GET /']);

// The embedded resource was still fresh
$this->assertSame(['fresh'], $traces['GET /foo']);
}

public function testEsiCacheIncludesEmbeddedResponseContentWhenMainResponseFailsRevalidationAndEmbeddedResponseIsValid()
{
$this->setNextResponses([
[
'status' => 200,
'body' => 'main <esi:include src="/foo" />',
'headers' => [
'Cache-Control' => 's-maxage=0', // goes stale immediately
'Surrogate-Control' => 'content="ESI/1.0"',
'Last-Modified' => 'Mon, 12 Aug 2024 10:00:00 +0000',
],
],
[
'status' => 200,
'body' => 'embedded',
'headers' => [
'Cache-Control' => 's-maxage=0', // goes stale immediately
'Last-Modified' => 'Mon, 12 Aug 2024 10:05:00 +0000',
]
],
]);

// prime the cache
$this->request('GET', '/', [], [], true);
$this->assertSame(200, $this->response->getStatusCode());
$this->assertSame('main embedded', $this->response->getContent());
$this->assertSame('Mon, 12 Aug 2024 10:05:00 +0000', $this->response->getLastModified()->format(\DATE_RFC2822)); // max of both values

$this->setNextResponses([
[
// On the next request, the main response has an updated Last-Modified (main page was modified)...
'status' => 200,
'body' => 'main <esi:include src="/foo" />',
'headers' => [
'Cache-Control' => 's-maxage=0',
'Surrogate-Control' => 'content="ESI/1.0"',
'Last-Modified' => 'Mon, 12 Aug 2024 10:10:00 +0000',
],
],
[
// We have a stale cache entry for the embedded response which will be revalidated.
// Let's assume the resource did not change, so the controller sends a 304 without content body.
'status' => 304,
'body' => '',
'headers' => [
'Cache-Control' => 's-maxage=0',
],
],
]);

// Re-request with Last-Modified time that we received when the cache was primed
$this->request('GET', '/', ['HTTP_IF_MODIFIED_SINCE' => 'Mon, 12 Aug 2024 10:05:00 +0000'], [], true);

$this->assertSame(200, $this->response->getStatusCode());

// The cache should use the content ("embedded") from the cached entry
$this->assertSame('main embedded', $this->response->getContent());

$traces = $this->cache->getTraces();
$this->assertSame(['stale', 'invalid', 'store'], $traces['GET /']);

// Check that the embedded resource was successfully revalidated
$this->assertSame(['stale', 'valid', 'store'], $traces['GET /foo']);
}

public function testEsiCacheIncludesEmbeddedResponseContentWhenMainAndEmbeddedResponseAreFresh()
{
$this->setNextResponses([
[
'status' => 200,
'body' => 'main <esi:include src="/foo" />',
'headers' => [
'Cache-Control' => 's-maxage=10',
'Surrogate-Control' => 'content="ESI/1.0"',
'Last-Modified' => 'Mon, 12 Aug 2024 10:05:00 +0000',
],
],
[
'status' => 200,
'body' => 'embedded',
'headers' => [
'Cache-Control' => 's-maxage=10',
'Last-Modified' => 'Mon, 12 Aug 2024 10:00:00 +0000',
]
],
]);

// prime the cache
$this->request('GET', '/', [], [], true);
$this->assertSame(200, $this->response->getStatusCode());
$this->assertSame('main embedded', $this->response->getContent());
$this->assertSame('Mon, 12 Aug 2024 10:05:00 +0000', $this->response->getLastModified()->format(\DATE_RFC2822));

// Assume that a client received 'Mon, 12 Aug 2024 10:00:00 +0000' as last-modified information in the past. This may, for example,
// be the case when the "main" response at that point had an older Last-Modified time, so the embedded response's Last-Modified time
// governed the result for the combined response. In other words, the client received a Last-Modified time that still validates the
// embedded response as of now, but no longer matches the Last-Modified time of the "main" resource.
// Now this client does a revalidation request.
$this->request('GET', '/', ['HTTP_IF_MODIFIED_SINCE' => 'Mon, 12 Aug 2024 10:00:00 +0000'], [], true);

$this->assertSame(200, $this->response->getStatusCode());

// The cache should use the content ("embedded") from the cached entry
$this->assertSame('main embedded', $this->response->getContent());

$traces = $this->cache->getTraces();
$this->assertSame(['fresh'], $traces['GET /']);

// Check that the embedded resource was successfully revalidated
$this->assertSame(['fresh'], $traces['GET /foo']);
}

public function testEsiCacheForceValidation()
{
$responses = [
Expand Down
Loading