Skip to content

Commit 8a42717

Browse files
committed
Merge pull request #207 from FriendsOfSymfony/etag
add support for a simple etag
2 parents b8d9786 + a02d058 commit 8a42717

File tree

11 files changed

+92
-21
lines changed

11 files changed

+92
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Changelog
1313

1414
**deprecated** `CacheManager::tagResponse` in favor of `TagHandler::addTags`
1515
* **2015-05-08** Added configuration option for custom proxy client (#208)
16+
* Added support for a simple Etag header in the header configuration (#207)
1617

1718
1.2.0
1819
-----

DependencyInjection/Configuration.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ private function addCacheControlSection(ArrayNodeDefinition $rootNode)
206206
->scalarNode('stale_while_revalidate')->end()
207207
->end()
208208
->end()
209+
->scalarNode('etag')
210+
->defaultValue(false)
211+
->info('Set a simple ETag which is just the md5 hash of the response body')
212+
->end()
209213
->scalarNode('last_modified')
210214
->validate()
211215
->ifTrue(function ($v) {

EventListener/CacheControlSubscriber.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ public function onKernelResponse(FilterResponseEvent $event)
130130
$response->setVary($options['vary'], $options['overwrite']);
131131
}
132132

133+
if (!empty($options['etag'])
134+
&& ($options['overwrite'] || null === $response->getEtag())
135+
) {
136+
$response->setEtag(md5($response->getContent()));
137+
}
133138
if (isset($options['last_modified'])
134139
&& ($options['overwrite'] || null === $response->getLastModified())
135140
) {

Resources/doc/features/headers.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ headers are added. You can force to overwrite the headers globally by setting
1717
``cache_control.defaults.overwrite: true`` to true, or on a per rule basis with
1818
``overwrite: true`` under ``headers``.
1919

20-
This is an example configuration. For more, see the
20+
This is an example configuration. For more, see the
2121
:doc:`/reference/configuration/headers` configuration reference.
2222

2323
.. code-block:: yaml
@@ -34,7 +34,7 @@ This is an example configuration. For more, see the
3434
host: ^login.example.com$
3535
headers:
3636
cache_control: { public: false, max_age: 0, s_maxage: 0 }
37-
last_modified: "-1 hour"
37+
etag: true
3838
vary: [Accept-Encoding, Accept-Language]
3939
4040
# match all actions of a specific controller
@@ -51,7 +51,7 @@ This is an example configuration. For more, see the
5151
path: ^/$
5252
headers:
5353
cache_control: { public: true, max_age: 64000, s_maxage: 64000 }
54-
last_modified: "-1 hour"
54+
etag: true
5555
vary: [Accept-Encoding, Accept-Language]
5656
5757
# match everything to set defaults
@@ -61,7 +61,7 @@ This is an example configuration. For more, see the
6161
headers:
6262
overwrite: false
6363
cache_control: { public: true, max_age: 15, s_maxage: 30 }
64-
last_modified: "-1 hour"
64+
etag: true
6565
6666
.. _manually setting cache headers: http://symfony.com/doc/current/book/http_cache.html#the-cache-control-header
6767
.. _setting caching headers through annotations: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/cache.html

Resources/doc/reference/configuration/headers.rst

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ parameters described in the ``match`` section, the headers as defined under
77
checked in the order specified, where the first match wins.
88

99
A global setting and a per rule ``overwrite`` option allow to overwrite the
10-
cache headers even if they are already set.
10+
cache headers even if they are already set:
1111

1212
.. code-block:: yaml
1313
@@ -27,7 +27,7 @@ cache headers even if they are already set.
2727
public: false
2828
max_age: 0
2929
s_maxage: 0
30-
last_modified: "-1 hour"
30+
etag: true
3131
vary: [Accept-Encoding, Accept-Language]
3232
3333
# match all actions of a specific controller
@@ -50,7 +50,7 @@ cache headers even if they are already set.
5050
public: true
5151
max_age: 64000
5252
s_maxage: 64000
53-
last_modified: "-1 hour"
53+
etag: true
5454
vary: [Accept-Encoding, Accept-Language]
5555
5656
# match everything to set defaults
@@ -62,7 +62,7 @@ cache headers even if they are already set.
6262
public: true
6363
max_age: 15
6464
s_maxage: 30
65-
last_modified: "-1 hour"
65+
etag: true
6666
6767
``rules``
6868
---------
@@ -143,7 +143,7 @@ You can use the standard cache control directives:
143143
* ``s_maxage`` time in seconds for proxy caches (also public caches);
144144
* ``private`` true or false;
145145
* ``public`` true or false;
146-
* ``no_cache`` true or false (use exclusively to support HTTP 1.0);
146+
* ``no_cache`` true or false (use exclusively to support HTTP 1.0).
147147

148148
.. code-block:: yaml
149149
@@ -166,23 +166,24 @@ default. If you want it respected, add your own logic to ``vcl_fetch``.
166166

167167
.. note::
168168

169-
The cache-control headers are described in detail in :rfc:`2616#section-14.9`.
169+
The cache-control headers are described in detail in :rfc:`2616#section-14.9`
170+
and further clarified in :rfc:`7234#section-5.2`.
170171

171172
Extra Cache Control Directives
172173
""""""""""""""""""""""""""""""
173174

174175
You can also set headers that Symfony considers non-standard, some coming from
175-
RFCs extending HTTP/1.1. The following options are supported:
176+
RFCs extending :rfc:`2616` HTTP/1.1. The following options are supported:
176177

177-
* ``must_revalidate`` (:rfc:`2616#section-14.9`)
178-
* ``proxy_revalidate`` (:rfc:`2616#section-14.9`)
179-
* ``no_transform`` (:rfc:`2616#section-14.9`)
180-
* ``stale_if_error``: seconds (:rfc:`5861`)
181-
* ``stale_while_revalidate``: seconds (:rfc:`5861`)
178+
* ``must_revalidate`` (:rfc:`7234#section-5.2.2.1`)
179+
* ``proxy_revalidate`` (:rfc:`7234#section-5.2.2.7`)
180+
* ``no_transform`` (:rfc:`7234#section-5.2.2.4`)
181+
* ``stale_if_error``: seconds (:rfc:`5861#section-4`)
182+
* ``stale_while_revalidate``: seconds (:rfc:`5861#section-3`)
182183

183184
The *stale* directives need a parameter specifying the time in seconds how long
184185
a cache is allowed to continue serving stale content if needed. The other
185-
directives are flags that are included when set to true.
186+
directives are flags that are included when set to true:
186187

187188
.. code-block:: yaml
188189
@@ -200,13 +201,46 @@ directives are flags that are included when set to true.
200201
proxy_revalidate: true
201202
no_transform: true
202203
204+
``etag``
205+
""""""""
206+
207+
**type**: ``boolean``
208+
209+
This enables a simplistic ETag calculated as md5 hash of the response body:
210+
211+
.. code-block:: yaml
212+
213+
# app/config/config.yml
214+
fos_http_cache:
215+
cache_control:
216+
rules:
217+
-
218+
headers:
219+
etag: true
220+
221+
.. tip::
222+
223+
This simplistic ETag handler will not help you to prevent unnecessary work
224+
on your web server, but allows a caching proxy to use the ETag cache
225+
validation method to preserve bandwidth. The presence of an ETag tells
226+
clients that they can send a ``If-None-Match`` header with the ETag their
227+
current version of the content has. If the caching proxy still has the same
228+
ETag, it responds with a "304 Not Modified" status.
229+
230+
You can get additional performance if you write your own ETag handler that
231+
can read an ETag from your content and decide very early in the request
232+
whether the ETag changed or not. It can then terminate the request early
233+
with an empty "304 Not Modified" response. This avoids rendering the whole
234+
page. If the page depends on permissions, make sure to make the ETag differ
235+
based on those permissions (e.g. by appending the :doc:`user context hash </features/user-context>`).
236+
203237
``last_modified``
204238
"""""""""""""""""
205239

206240
**type**: ``string``
207241

208242
The input to the ``last_modified`` is used for the ``Last-Modified`` header.
209-
This value must be a valid input to ``DateTime``.
243+
This value must be a valid input to ``DateTime``:
210244

211245
.. code-block:: yaml
212246
@@ -218,20 +252,36 @@ This value must be a valid input to ``DateTime``.
218252
headers:
219253
last_modified: "-1 hour"
220254
221-
.. hint::
255+
.. note::
222256

223257
Setting an arbitrary last modified time allows clients to send
224258
``If-Modified-Since`` requests. Varnish can handle these to serve data
225259
from the cache if it was not invalidated since the client requested it.
226260

261+
Note that the default system will generate an arbitrary last modified date.
262+
You can get additional performance if you write your own last modified
263+
handler that can compare this date with information about the content of
264+
your page and decide early in the request whether anything changed. It can
265+
then terminate the request early with an empty "304 Not Modified" response.
266+
Using content meta data increases the probability for a 304 response and
267+
avoids rendering the whole page.
268+
269+
See also :rfc:`7232#section-2.2.1` for further consideration on how to
270+
generate the last modified date.
271+
272+
.. note::
273+
274+
You may configure both ETag and last modified on the same response. See
275+
:rfc:`7232#section-2.4` for more details.
276+
227277
``vary``
228278
""""""""
229279

230280
**type**: ``string``
231281

232282
You can set the `vary` option to an array that defines the contents of the
233283
`Vary` header when matching the request. This adds to existing Vary headers,
234-
keeping previously set Vary options.
284+
keeping previously set Vary options:
235285

236286
.. code-block:: yaml
237287

Resources/doc/spelling_word_list.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ github
88
subdomains
99
yaml
1010
invalidator
11+
ETag

Tests/Resources/Fixtures/config/full.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
'stale_if_error' => 3,
2929
'stale_while_revalidate' => 4,
3030
),
31+
'etag' => true,
3132
'last_modified' => '-1 hour',
3233
'reverse_proxy_ttl' => 42,
3334
'vary' => array('Cookie', 'Authorization'),

Tests/Resources/Fixtures/config/full.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<additional-cacheable-status>100</additional-cacheable-status>
2020
<additional-cacheable-status>500</additional-cacheable-status>
2121
</match>
22-
<headers last-modified="-1 hour" reverse-proxy-ttl="42">
22+
<headers etag="true" last-modified="-1 hour" reverse-proxy-ttl="42">
2323
<overwrite>false</overwrite>
2424
<cache-control
2525
max-age="1"

Tests/Resources/Fixtures/config/full.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ fos_http_cache:
3131
no_cache: false
3232
stale_if_error: 3
3333
stale_while_revalidate: 4
34+
etag: true
3435
last_modified: -1 hour
3536
reverse_proxy_ttl: 42
3637
vary:

Tests/Unit/DependencyInjection/ConfigurationTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public function testSupportsAllConfigFormats()
7777
'stale_if_error' => 3,
7878
'stale_while_revalidate' => 4,
7979
),
80+
'etag' => true,
8081
'last_modified' => '-1 hour',
8182
'reverse_proxy_ttl' => 42,
8283
'vary' => array('Cookie', 'Authorization'),
@@ -232,6 +233,7 @@ public function testSplitOptions()
232233
'reverse_proxy_ttl' => null,
233234
'vary' => array('Cookie', 'Authorization'),
234235
'overwrite' => 'default',
236+
'etag' => false,
235237
),
236238
),
237239
),

Tests/Unit/EventListener/CacheControlSubscriberTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ public function testMergeHeaders()
128128
'vary' => array(
129129
'Cookie',
130130
),
131+
'etag' => true,
131132
'last_modified' => '2014-10-10 GMT',
132133
);
133134
$subscriber = $this->getCacheControl($headers);
@@ -136,13 +137,15 @@ public function testMergeHeaders()
136137
$response->setCache(array('max_age' => 0));
137138
$response->headers->addCacheControlDirective('stale-if-error', 0);
138139
$response->setVary('Encoding');
140+
$response->setEtag('foo');
139141
$response->setLastModified(new \DateTime('2013-09-09 GMT'));
140142

141143
$subscriber->onKernelResponse($event);
142144
$newHeaders = $event->getResponse()->headers->all();
143145

144146
$this->assertEquals('max-age=0, must-revalidate, no-transform, proxy-revalidate, public, s-maxage=300, stale-if-error=0, stale-while-revalidate=400', $newHeaders['cache-control'][0]);
145147
$this->assertEquals(array('Encoding', 'Cookie'), $newHeaders['vary']);
148+
$this->assertEquals('"foo"', $newHeaders['etag'][0]);
146149
$this->assertEquals('Mon, 09 Sep 2013 00:00:00 GMT', $newHeaders['last-modified'][0]);
147150
}
148151

@@ -165,6 +168,7 @@ public function testOverwriteHeaders()
165168
'vary' => array(
166169
'Cookie',
167170
),
171+
'etag' => true,
168172
'last_modified' => '2014-10-10 GMT',
169173
);
170174
$subscriber = $this->getCacheControl($headers);
@@ -173,13 +177,15 @@ public function testOverwriteHeaders()
173177
$response->setCache(array('max_age' => 0));
174178
$response->headers->addCacheControlDirective('stale-if-error', 0);
175179
$response->setVary('Encoding');
180+
$response->setEtag('foo');
176181
$response->setLastModified(new \DateTime('2013-09-09 GMT'));
177182

178183
$subscriber->onKernelResponse($event);
179184
$newHeaders = $event->getResponse()->headers->all();
180185

181186
$this->assertEquals('max-age=900, must-revalidate, no-transform, proxy-revalidate, public, s-maxage=300, stale-if-error=300, stale-while-revalidate=400', $newHeaders['cache-control'][0]);
182187
$this->assertEquals(array('Cookie'), $newHeaders['vary']);
188+
$this->assertEquals('"'.md5('').'"', $response->getEtag());
183189
$this->assertEquals('Fri, 10 Oct 2014 00:00:00 GMT', $newHeaders['last-modified'][0]);
184190
}
185191

0 commit comments

Comments
 (0)