# PlugHTTPCache
A Plug that caches HTTP responses
This plug library relies on the [`http_cache`](https://hexdocs.pm/http_cache)
library. It supports all caching features of
[RFC9111](https://datatracker.ietf.org/doc/html/rfc9111) and more
(such as conditional requests and range requests).
See [`http_cache`](https://hexdocs.pm/http_cache/) documentation for more information.
![Screenshot of pug_http_cache_demo Grafana dashboard](https://raw.githubusercontent.com/tanguilp/plug_http_cache/master/media/grafana.png)
Screenshot from the [`plug_http_cache_demo`](https://github.com/tanguilp/plug_http_cache_demo)
application.
## Installation
```elixir
def deps do
[
{:http_cache, "~> 0.3.0"},
{:plug_http_cache, "~> 0.3.0"}
]
end
```
## Configuration
In your plug pipeline, set the Plug for routes on which you want to enable caching:
`router.ex`
```elixir
pipeline :cache do
plug PlugHTTPCache, @caching_options
end
...
scope "/", PlugHTTPCacheDemoWeb do
pipe_through :browser
scope "/some_route" do
pipe_through :cache
...
end
end
```
You can also configure it for all requests by setting it in Phoenix's endpoint
file:
`endpoint.ex`
```elixir
defmodule MyApp.Endpoint do
use Phoenix.Endpoint, otp_app: :plug_http_cache_demo
% some other plugs
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.Head
plug Plug.Session, @session_options
plug PlugHTTPCache, @caching_options
plug PlugHTTPCacheDemoWeb.Router
end
```
Note that:
- caching chunked responses is *not* supported
- some responses (called "cacheable by default") can be cached even when no
`cache-control` header is set. For instance, a 200 response to a get request is
cached 2 minutes by default, unless `cache-control` headers prohibit it
- Phoenix automatically sets the `"cache-control"` header to
`"max-age=0, private, must-revalidate"`, so by default no response will ever
be cached unless you override this header
You can also configure `PlugHTTPCache.StaleIfError` to return expired cached responses.
This is useful to continue returning something when the backend experiences failures
(for example if the DB crashed and while it's rebooting).
## Plug options
Plug options are those documented by
[`:http_cache.opts/0`](https://hexdocs.pm/http_cache/http_cache.html#t:opts/0).
The only required option is `:store`.
This plug sets the following default options:
- `:type`: `:shared`,
- `:auto_compress`: `true`,
- `:auto_accept_encoding`: `true`
## Stores
Responses have to be stored in a separate store backend (this library does not come with one), such
as:
- [`http_cache_store_memory`](https://github.com/tanguilp/http_cache_store_memory): responses are
stored in memory (ETS)
- [`http_cache_store_disk`](https://github.com/tanguilp/http_cache_store_disk): responses are
stored on disk. This library uses the `sendfile` system call and therefore benefits from the kernel's
memory caching automatically
Both are cluster-aware.
To use it along with this library, just add it to your mix.exs file:
`mix.exs`
```elixir
{:http_cache, "~> ..."},
{:plug_http_cache, "~> ..."},
{:http_cache_store_memory, "~> ..."},
```
## Security considerations
Unlike many HTTP caches, `http_cache` allows caching:
- responses to authorized request (with an `"authorization"` header)
- responses with cookies
In the first case, beware of authenticating before handling caching. In
other words, **don't**:
```elixir
PlugHTTPCache, @caching_options
MyPlug.AuthorizeUser
```
which would return a cached response to unauthorized users, but **do** instead:
```elixir
MyPlug.AuthorizeUser
PlugHTTPCache, @caching_options
```
Beware of not setting caching headers on private responses containing cookies.
## Useful libraries
- [`PlugCacheControl`](https://github.com/krasenyp/plug_cache_control) can be used
to set cache-control headers in your Plug pipelines, or manually in your controllers
- [`PlugHTTPValidator`](https://github.com/tanguilp/plug_http_validator) *should* be used
to set HTTP validators as soon as cacheable content is returned. See project
documentation to figure out why
## Telemetry events
The following events are emitted:
- `[:plug_http_cache, :hit]` when a cached response is returned.
- `[:plug_http_cache, :miss]` when no cached response was found
- `[:plug_http_cache, :stale_if_error]` when a response was returned because an error
occurred downstream (see `PlugHTTPCache.StaleIfError`)
Neither measurements nor metadata are added to these events.
The `http_cache`, `http_cache_store_memory` and `http_cache_store_disk` emit other events about
the caching subsystems, including some helping with detecting normalization issues.
## Normalization
The underlying http caching library may store different responses for the same URL,
following the directives of the `"vary"` header. For instance, if a response can
be returned in English or in French, both versions can be cached as long as the
`"vary"` header is correctly used.
This can unfortunately result in an explosion of stored responses if the headers
are not normalized. For instance, in this scenario where a site handles both these
languages, a response will be stored for any of these requests that include an
`"accept-language"` header:
- fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5
- fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7,*;q=0.5
- en
- de
- en, de
- en, de, fr
- en;q=1, de
- en;q=1, de;q=0.9
- en;q=1, de;q=0.8
- en;q=1, de;q=0.7
- en;q=1, de;q=0.6
- en;q=1, de;q=0.5
and so on, so potentially hundreds of stored responses for only 2 available
responses (English and French versions).
In this case, you probably want to apply normalization before caching. This
could be done by a plug set before the `PlugHTTPCache` plug.
See [Best practices for using the Vary header](https://www.fastly.com/blog/best-practices-using-vary-header)
for more guidance regarding this issue.