bug #33487 [HttpKernel] Fix Apache mod_expires Session Cache-Control issue (pbowyer)

This PR was squashed before being merged into the 3.4 branch (closes #33487).

Discussion
----------

[HttpKernel] Fix Apache mod_expires Session Cache-Control issue

| Q             | A
| ------------- | ---
| Branch?       | 3.4 for bug fixes <!-- see below -->
| Bug fix?      | yes
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| License       | MIT

Apaches's [mod_expires](https://httpd.apache.org/docs/current/mod/mod_expires.html) is a widely used module to set HTTP caching headers. It allows you to set a default cache lifetime as well as lifetimes by mime_type.

When an application server has set a `Cache-Control` header, mod_expires ignores this and sets its own, resulting in duplicate `Cache-Control` headers and conflicting information. It does this _unless_ the application server sets an `Expires` header, in which case mod_expires does nothing. This is documented on the link above:

> When the `Expires` header is already part of the response generated by the server, for example when generated by a CGI script or proxied from an origin server, this module does not change or add an `Expires` or `Cache-Control` header.

Symfony automatically sets a `Cache-Control` header if a session exists. This patch adds an `Expires` header to ensure it's respected by mod_expires.

## Example 1
With the following Apache config:
```apache
<IfModule mod_expires.c>
    ExpiresActive on
    ExpiresDefault                                      "access plus 1 month"
</IfModule>
```
The HTTP response headers are:
### Without the patch
```
HTTP/1.1 200 OK
Date: Fri, 06 Sep 2019 08:02:02 GMT
Server: Apache/2.4.37 (Ubuntu)
Cache-Control: max-age=0, must-revalidate, private
Cache-Control: max-age=2592000
Expires: Sun, 06 Oct 2019 08:02:00 GMT
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 13099
Connection: close
Content-Type: text/html; charset=UTF-8
```
### With the patch
```
HTTP/1.1 200 OK
Date: Fri, 06 Sep 2019 08:21:34 GMT
Server: Apache/2.4.37 (Ubuntu)
Cache-Control: max-age=0, must-revalidate, private
Expires: Fri, 06 Sep 2019 08:21:34 GMT
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 13098
Connection: close
Content-Type: text/html; charset=UTF-8
```

## Example 2
With the following Apache config:
```apache
<IfModule mod_expires.c>
    ExpiresActive on
    ExpiresDefault                                      "access plus 1 month"
    ExpiresByType text/html                             "access plus 0 seconds"
</IfModule>
```
### Without the patch
```
HTTP/1.1 200 OK
Date: Fri, 06 Sep 2019 08:18:40 GMT
Server: Apache/2.4.37 (Ubuntu)
Cache-Control: max-age=0, must-revalidate, private
Cache-Control: max-age=0
Expires: Fri, 06 Sep 2019 08:18:39 GMT
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 13099
Connection: close
Content-Type: text/html; charset=UTF-8
```
### With the patch
```
HTTP/1.1 200 OK
Date: Fri, 06 Sep 2019 08:20:40 GMT
Server: Apache/2.4.37 (Ubuntu)
Cache-Control: max-age=0, must-revalidate, private
Expires: Fri, 06 Sep 2019 08:20:40 GMT
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 13100
Connection: close
Content-Type: text/html; charset=UTF-8
```

Commits
-------

9e942768c9 [HttpKernel] Fix Apache mod_expires Session Cache-Control issue
This commit is contained in:
Fabien Potencier 2019-09-08 08:51:05 +02:00
commit 206ad498c1
2 changed files with 9 additions and 0 deletions

View File

@ -56,6 +56,7 @@ abstract class AbstractSessionListener implements EventSubscriberInterface
if ($session instanceof Session ? $session->getUsageIndex() !== end($this->sessionUsageStack) : $session->isStarted()) {
$event->getResponse()
->setExpires(new \DateTime())
->setPrivate()
->setMaxAge(0)
->headers->addCacheControlDirective('must-revalidate');

View File

@ -75,6 +75,9 @@ class SessionListenerTest extends TestCase
$this->assertTrue($response->headers->hasCacheControlDirective('private'));
$this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate'));
$this->assertSame('0', $response->headers->getCacheControlDirective('max-age'));
$this->assertTrue($response->headers->has('Expires'));
$this->assertLessThanOrEqual((new \DateTime('now', new \DateTimeZone('UTC'))), (new \DateTime($response->headers->get('Expires'))));
}
public function testSurrogateMasterRequestIsPublic()
@ -104,10 +107,15 @@ class SessionListenerTest extends TestCase
$this->assertFalse($response->headers->hasCacheControlDirective('must-revalidate'));
$this->assertSame('30', $response->headers->getCacheControlDirective('max-age'));
$this->assertFalse($response->headers->has('Expires'));
$listener->onKernelResponse(new FilterResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response));
$this->assertTrue($response->headers->hasCacheControlDirective('private'));
$this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate'));
$this->assertSame('0', $response->headers->getCacheControlDirective('max-age'));
$this->assertTrue($response->headers->has('Expires'));
$this->assertLessThanOrEqual((new \DateTime('now', new \DateTimeZone('UTC'))), (new \DateTime($response->headers->get('Expires'))));
}
}