mirror of
https://github.com/symfony/symfony-docs.git
synced 2026-03-24 00:32:14 +01:00
228 lines
9.2 KiB
ReStructuredText
228 lines
9.2 KiB
ReStructuredText
HTTP Cache Validation
|
|
=====================
|
|
|
|
When a resource needs to be updated as soon as a change is made to the underlying
|
|
data, the expiration model falls short. With the `expiration model`_, the
|
|
application won't be asked to return the updated response until the cache
|
|
finally becomes stale.
|
|
|
|
The `validation model`_ addresses this issue. Under this model, the cache continues
|
|
to store responses. The difference is that, for each request, the cache asks the
|
|
application if the cached response is still valid or if it needs to be regenerated.
|
|
If the cache *is* still valid, your application should return a 304 status code
|
|
and no content. This tells the cache that it's OK to return the cached response.
|
|
|
|
Under this model, you only save CPU if you're able to determine that the
|
|
cached response is still valid by doing *less* work than generating the whole
|
|
page again (see below for an implementation example).
|
|
|
|
.. tip::
|
|
|
|
The 304 status code means "Not Modified". It's important because with
|
|
this status code the response does *not* contain the actual content being
|
|
requested. Instead, the response only consists of the response headers that
|
|
tells the cache that it can use its stored version of the content.
|
|
|
|
Like with expiration, there are two different HTTP headers that can be used
|
|
to implement the validation model: ``ETag`` and ``Last-Modified``.
|
|
|
|
.. include:: /http_cache/_expiration-and-validation.rst.inc
|
|
|
|
Validation with the ``ETag`` Header
|
|
-----------------------------------
|
|
|
|
The `HTTP ETag`_ ("entity-tag") header is an optional HTTP header whose value is
|
|
an arbitrary string that uniquely identifies one representation of the target
|
|
resource. It's entirely generated and set by your application so that you can
|
|
tell, for example, if the ``/about`` resource that's stored by the cache is
|
|
up-to-date with what your application would return.
|
|
|
|
An ``ETag`` is like a fingerprint and is used to quickly compare if two
|
|
different versions of a resource are equivalent. Like fingerprints, each
|
|
``ETag`` must be unique across all representations of the same resource.
|
|
|
|
To see a short implementation, generate the ``ETag`` as the ``md5`` of the
|
|
content::
|
|
|
|
// src/Controller/DefaultController.php
|
|
namespace App\Controller;
|
|
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class DefaultController extends AbstractController
|
|
{
|
|
public function homepage(Request $request): Response
|
|
{
|
|
$response = $this->render('static/homepage.html.twig');
|
|
$response->setEtag(md5($response->getContent()));
|
|
$response->setPublic(); // make sure the response is public/cacheable
|
|
$response->isNotModified($request);
|
|
|
|
return $response;
|
|
}
|
|
}
|
|
|
|
The :method:`Symfony\\Component\\HttpFoundation\\Response::isNotModified`
|
|
method compares the ``If-None-Match`` header with the ``ETag`` response header.
|
|
If the two match, the method automatically sets the ``Response`` status code
|
|
to 304.
|
|
|
|
.. note::
|
|
|
|
When using ``mod_deflate`` or ``mod_brotli`` in Apache 2.4, the original
|
|
``ETag`` value is modified (e.g. if ``ETag`` was ``foo``, Apache turns it
|
|
into ``foo-gzip`` or ``foo-br``), which breaks the ``ETag``-based validation.
|
|
|
|
You can control this behavior with the `DeflateAlterETag`_ and `BrotliAlterETag`_
|
|
directives. Alternatively, you can use the following Apache configuration to
|
|
keep both the original ``ETag`` and the modified one when compressing responses:
|
|
|
|
.. code-block:: apache
|
|
|
|
RequestHeader edit "If-None-Match" '^"((.*)-(gzip|br))"$' '"$1", "$2"'
|
|
|
|
.. note::
|
|
|
|
The cache sets the ``If-None-Match`` header on the request to the ``ETag``
|
|
of the original cached response before sending the request back to the
|
|
app. This is how the cache and server communicate with each other and
|
|
decide whether or not the resource has been updated since it was cached.
|
|
|
|
This algorithm works and is very generic, but you need to create the whole
|
|
``Response`` before being able to compute the ``ETag``, which is sub-optimal.
|
|
In other words, it saves on bandwidth, but not CPU cycles.
|
|
|
|
In the :ref:`optimizing-cache-validation` section, you'll see how validation
|
|
can be used more intelligently to determine the validity of a cache without
|
|
doing so much work.
|
|
|
|
.. tip::
|
|
|
|
Symfony also supports weak ``ETag`` s by passing ``true`` as the second
|
|
argument to the
|
|
:method:`Symfony\\Component\\HttpFoundation\\Response::setEtag` method.
|
|
|
|
Validation with the ``Last-Modified`` Header
|
|
--------------------------------------------
|
|
|
|
The ``Last-Modified`` header is the second form of validation. According
|
|
to the HTTP specification, "The ``Last-Modified`` header field indicates
|
|
the date and time at which the origin server believes the representation
|
|
was last modified." In other words, the application decides whether or not
|
|
the cached content has been updated based on whether or not it's been updated
|
|
since the response was cached.
|
|
|
|
For instance, you can use the latest update date for all the objects needed to
|
|
compute the resource representation as the value for the ``Last-Modified``
|
|
header value::
|
|
|
|
// src/Controller/ArticleController.php
|
|
namespace App\Controller;
|
|
|
|
// ...
|
|
use App\Entity\Article;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class ArticleController extends AbstractController
|
|
{
|
|
public function show(Article $article, Request $request): Response
|
|
{
|
|
$author = $article->getAuthor();
|
|
|
|
$articleDate = new \DateTime($article->getUpdatedAt());
|
|
$authorDate = new \DateTime($author->getUpdatedAt());
|
|
|
|
$date = $authorDate > $articleDate ? $authorDate : $articleDate;
|
|
|
|
$response = new Response();
|
|
$response->setLastModified($date);
|
|
// Set response as public. Otherwise it will be private by default.
|
|
$response->setPublic();
|
|
|
|
if ($response->isNotModified($request)) {
|
|
return $response;
|
|
}
|
|
|
|
// ... do more work to populate the response with the full content
|
|
|
|
return $response;
|
|
}
|
|
}
|
|
|
|
The :method:`Symfony\\Component\\HttpFoundation\\Response::isNotModified`
|
|
method compares the ``If-Modified-Since`` header with the ``Last-Modified``
|
|
response header. If they are equivalent, the ``Response`` will be set to a
|
|
304 status code.
|
|
|
|
.. note::
|
|
|
|
The cache sets the ``If-Modified-Since`` header on the request to the ``Last-Modified``
|
|
of the original cached response before sending the request back to the
|
|
app. This is how the cache and server communicate with each other and
|
|
decide whether or not the resource has been updated since it was cached.
|
|
|
|
.. _optimizing-cache-validation:
|
|
|
|
Optimizing your Code with Validation
|
|
------------------------------------
|
|
|
|
The main goal of any caching strategy is to lighten the load on the application.
|
|
Put another way, the less you do in your application to return a 304 response,
|
|
the better. The ``Response::isNotModified()`` method does exactly that::
|
|
|
|
// src/Controller/ArticleController.php
|
|
namespace App\Controller;
|
|
|
|
// ...
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class ArticleController extends AbstractController
|
|
{
|
|
public function show(string $articleSlug, Request $request): Response
|
|
{
|
|
// Get the minimum information to compute
|
|
// the ETag or the Last-Modified value
|
|
// (based on the Request, data is retrieved from
|
|
// a database or a key-value store for instance)
|
|
$article = ...;
|
|
|
|
// create a Response with an ETag and/or a Last-Modified header
|
|
$response = new Response();
|
|
$response->setEtag($article->computeETag());
|
|
$response->setLastModified($article->getPublishedAt());
|
|
|
|
// Set response as public. Otherwise it will be private by default.
|
|
$response->setPublic();
|
|
|
|
// Check that the Response is not modified for the given Request
|
|
if ($response->isNotModified($request)) {
|
|
// return the 304 Response immediately
|
|
return $response;
|
|
}
|
|
|
|
// do more work here - like retrieving more data
|
|
$comments = ...;
|
|
|
|
// or render a template with the $response you've already started
|
|
return $this->render('article/show.html.twig', [
|
|
'article' => $article,
|
|
'comments' => $comments,
|
|
], $response);
|
|
}
|
|
}
|
|
|
|
When the ``Response`` is not modified, the ``isNotModified()`` automatically sets
|
|
the response status code to ``304``, removes the content, and removes some
|
|
headers that must not be present for ``304`` responses (see
|
|
:method:`Symfony\\Component\\HttpFoundation\\Response::setNotModified`).
|
|
|
|
.. _`expiration model`: https://tools.ietf.org/html/rfc2616#section-13.2
|
|
.. _`validation model`: https://tools.ietf.org/html/rfc2616#section-13.3
|
|
.. _`HTTP ETag`: https://en.wikipedia.org/wiki/HTTP_ETag
|
|
.. _`DeflateAlterETag`: https://httpd.apache.org/docs/trunk/mod/mod_deflate.html#deflatealteretag
|
|
.. _`BrotliAlterETag`: https://httpd.apache.org/docs/2.4/mod/mod_brotli.html#brotlialteretag
|