Skip to content

Commit 62f3347

Browse files
committed
add support for a simple etag
1 parent a1e22ce commit 62f3347

File tree

11 files changed

+76
-14
lines changed

11 files changed

+76
-14
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+
* **2015-05-08** 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: 50 additions & 9 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
@@ -182,7 +182,7 @@ RFCs extending HTTP/1.1. The following options are supported:
182182

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

187187
.. code-block:: yaml
188188
@@ -200,13 +200,44 @@ directives are flags that are included when set to true.
200200
proxy_revalidate: true
201201
no_transform: true
202202
203+
``etag``
204+
""""""""
205+
206+
**type**: ``boolean``
207+
208+
This enables a simplistic ETag calculated as md5 hash of the response body:
209+
210+
.. code-block:: yaml
211+
212+
# app/config/config.yml
213+
fos_http_cache:
214+
cache_control:
215+
rules:
216+
-
217+
headers:
218+
etag: true
219+
220+
.. tip::
221+
222+
This simplistic ETag handler will not help you to prevent unnecessary work
223+
on your webserver, but allows a caching proxy to use the ETag cache
224+
validation method to preserve bandwidth. The presence of an ETag tells
225+
clients that they can send a ``If-None-Match`` header with the ETag their
226+
current version of the content has. If the caching proxy still has the same
227+
ETag, it responds with a "304 Not Modified" status.
228+
229+
You can get additional performance if you write your own ETag listener that
230+
also checks the ETag on requests and decides whether the content of the
231+
requested page would still generate the same ETag and abort the request as
232+
early as possible to return the "304 Not Modified" status.
233+
203234
``last_modified``
204235
"""""""""""""""""
205236

206237
**type**: ``string``
207238

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

211242
.. code-block:: yaml
212243
@@ -218,20 +249,30 @@ This value must be a valid input to ``DateTime``.
218249
headers:
219250
last_modified: "-1 hour"
220251
221-
.. hint::
252+
.. note::
222253

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

258+
If you only use these simple configurations to add cache validation
259+
information, there is no point in adding both a last modified and ETag
260+
header. Usually, you should prefer the ETag method as it is not prone to
261+
clock skew issues.
262+
263+
If your site is under heavy load, your best option is to write either a
264+
custom ETag handler or a custom last modified handler that uses the content
265+
of the page in a way that can avoid rendering the whole page before
266+
deciding whether the cached version is still valid.
267+
227268
``vary``
228269
""""""""
229270

230271
**type**: ``string``
231272

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

236277
.. code-block:: yaml
237278

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)