Skip to content

Fix Apache's mod_expires Cache-Control issue #24

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

Closed
wants to merge 1 commit into from
Closed

Fix Apache's mod_expires Cache-Control issue #24

wants to merge 1 commit into from

Conversation

pbowyer
Copy link
Contributor

@pbowyer pbowyer commented Sep 6, 2019

Apaches's mod_expires 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 ensures it's respected by mod_expires.

Example 1

With the following Apache config:

<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:

<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

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 ensures 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
```
@symfony-bot
Copy link

symfony-bot bot commented Sep 6, 2019

Thanks for your pull request! We love contributions.

However, this repository is what we call a "subtree split": a read-only copy of one directory of the main Symfony repository. It is used by Composer to allow developers to depend on specific Symfony components.

If you want to contribute, you should instead open a pull request on the main repository:

https://github.com/symfony/symfony

Thank you for your contribution!

PS: if you haven't already, please add tests, and beware that bug fixes should be submitted on the lowest maintained branch where they apply; only features should be submitted against the master branch.

@symfony-bot symfony-bot bot closed this Sep 6, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant