English spoken conference

HTTP Cache

HTTP Cache

HTTP Cache is usually managed at an additional layer on top of the web server (Varnish, Cloudflare, Cloudfront...). SymfonyCloud also allows you to enable HTTP caching right at the web server level.

When the cache is enabled, a fresh response is first looked for in the cache; if not available, the request is routed to the application.

Cache can be configured in the .symfony/routes.yaml file like below:

1
2
3
4
5
6
7
8
https://{default}/:
    type: upstream
    upstream: app:http
    cache:
        enabled: true
        headers: ['Accept', 'Accept-Language', 'X-Language-Locale']
        cookies: ['*']
        default_ttl: 10

Caution

By default, responses issued with Symfony are not cacheable (no-cache, private). To make use of SymfonyCloud cache, set a shared max-age on responses:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// src/Controller/DefaultController.php
use Symfony\Component\HttpFoundation\Response;
// ...

public function index()
{
    $response = $this->render('index.html.twig');

    // sets the shared max age - which also marks the response as public
    $response->setSharedMaxAge(3600);

    return $response;
}

Note

For more information about HTTP cache with Symfony, you can read its official documentation.

Cache per Route

If you need fine-grained caching, set caching rules separately for each route:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
https://{default}/:
    type: upstream
    upstream: php:http
    cache:
        enabled: true

https://{default}/foo:
    type: upstream
    upstream: php:http
    cache:
        enabled: false

https://{default}/foo/bar:
    type: upstream
    upstream: php:http
    cache:
        enabled: true

With this configuration, the following routes are cached:

  • http://{default}/
  • http://{default}/foo/bar
  • http://{default}/foo/bar/baz

And the following routes are not cached:

  • http://{default}/foo
  • http://{default}/foo/baz

Note

To use WebSocket, cache must be disabled because WebSocket is incompatible with buffering, which is a requirement of our caching technology.

Cache Behavior

Cache is only applied to GET and HEAD requests. Responses with the Cache-Control header set to Private, No-Cache, or No-Store are not cached. Responses with the Set-Cookie header are also not cached.

The web server does not honor the Pragma request header. Conditional requests using If-Modified-Since and If-None-Match are both supported.

Cache Revalidation

When the cache is expired, the web server will route the request to the application adding an If-Modified-Since header.

Cache Key

When determining if a response is cached, in addition to the request URI, SymfonyCloud builds a cache key. The cache key is built using the values of the request Headers and Cookies and used to differentiate several requests to the same URI.

The configuration lets you change which headers and cookies are used to build the cache key. Default values for these parameters are:

1
2
3
4
cache:
    enabled: true
    cookies: ['*']
    headers: ['Accept', 'Accept-Language']

The Vary header in the response is also respected: multiple copies are stored in the cache according to the value of this header. For example, by adding Vary: X-Forwarded-Proto to the response header, responses contents are cached but differentiated based on the request protocol (i.e. HTTP or HTTPS).

Cache Configuration Reference

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
https://{default}/:
    type: upstream
    upstream: php:http
    cache:
        enabled: true
        headers: ['Accept']
        cookies: ['/^__utm/', '_ga', '_gat', '_gid']
        default_ttl: 10

https://{default}/admin:
    type: upstream
    upstream: php:http
    cache:
        enabled: false

enabled

  • true : enable the cache for this route (default)
  • false : disable the cache for this route

headers

The headers parameter defines which values the cache key depends on.

For example, the following cache configuration caches a different response for each value of the Accept HTTP request header:

1
2
3
cache:
    enabled: true
    headers: ['Accept']

The default value is ['Accept', 'Accept-Language']

Note

The following request headers cannot be used as cache key: Accept-Encoding, Connection, Proxy-Authorization, TE, Upgrade.

cookies

The cookies parameter defines which values the cache key must depend on.

For example, with the following cache configuration; the cache key will depend on the value of the foo cookie in the request; other cookies will be ignored:

1
2
3
cache:
    enabled: true
    cookies: ['foo']

A special case is the ['*'] value: makes any request with a cookie bypass the cache. This is the default value.

[] ignores all cookies.

Note

Regular expressions can be used for cookie names. Entries that begin and end with a / are interpreted as PCRE regexes and evaluated against the cookie name.

default_ttl

Defines the default cache duration, in seconds. The cache duration is decided based on the Cache-Control response header value. If no Cache-Control header is in the response, then the value of the default_ttl parameter is used. The default value is 0.

SSI

SSI allows cache strategies to differ between the whole page and some more-dynamic parts of an HTML page thanks to directives similar to HTML tags:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!DOCTYPE html>
<html>
    <body>
        <!-- ... some content -->

        <!-- Embed the content of another page here -->
        <!--#include virtual="http://..." -->

        <!-- ... more content -->
    </body>
</html>

Enable SSI in .symfony/routes.yaml:

1
2
3
4
5
https://{default}/:
    type: upstream
    upstream: app:http
    ssi:
        enabled: true

And the SSI and fragments support in the Symfony configuration:

1
2
3
4
5
# config/packages/framework.yaml
framework:
    # ...
    ssi: true
    fragments: true

Now, suppose you have a page that is relatively static, except for the current date included at the top of the content. With SSI, you can cache the date independently of the rest of the page:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Controller/DefaultController.php

use Symfony\Component\HttpFoundation\Response;
// ...

public function index()
{
    $response = $this->render('index.html.twig');

    // sets the shared max age - which also marks the response as public
    $response->setSharedMaxAge(3600);

    return $response;
}

public function date()
{
    $response = $this->render('date.html.twig');

    return $response;
}
1
2
3
4
5
6
7
{# templates/index.html.twig #}

{# you can use a controller reference #}
{{ render_ssi(controller('App\\Controller\\DefaultController::date')) }}

{# ... or a URL #}
{{ render_ssi(url('date')) }}

With SSI, the full page cache is valid for 600 seconds, but the date component is not cached.

To dig further, you can read about SSI on Wikipedia.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.