defmodule PlugResponseCache do @moduledoc """ The response cache plug can cache an entire response body for a set amount of time. To enable the cache, add the plug to your pipeline as early as possible. Here's what that looks like in a clean Phoenix installation: pipeline :browser do plug(:accepts, ["html"]) plug(PlugResponseCache) plug(:fetch_session) plug(:fetch_flash) plug(:protect_from_forgery) plug(:put_secure_browser_headers) end The plug should be configured with the `:plug_response_cache` config key. This belongs in your application's configuration file: config :plug_response_cache, enabled: false, store: MyApp.ResponseCache.MyCustomStore profile: MyApp.ResponseCache.MyCustomProfile - `enabled`: Enables or disables the response cache entirely (default: `true`) - `store`: A custom cache store — a module that implements `PlugResponseCache.Store` callbacks (default: `PlugResponseCache.Stores.Ets`) - `profile`: A custom cache profile — a module that implements `PlugResponseCache.Profile` callbacks (default: `PlugResponseCache.Profiles.Default`) - `debug`: Logs the response cache's result on every request when `true` (default: `false`) Since every option has a default, the response cache will also work without specifying any custom configuration. Options are also passed to the cache profile, so if a profile has options simply add them to the list. For example, the default profile (`PlugResponseCache.Profiles.Default`) has an `expiration_time` option to determine for how many minutes the response should be cached. config :plug_response_cache, expiration_time: 5 It's also possible to pass options to the plug. This is useful for enabling or disabling the response cache on the fly, or for toggling the debug mode. pipeline :browser do # ... plug(PlugResponseCache, debug: true) # ... end It's not recommended to configure the profile & store like this because they might require a supervisor, which needs to be known ahead of time. """ @type expire_time :: DateTime.t() | :never @type hit :: {:hit, {Plug.Conn.status(), Plug.Conn.body(), expire_time()}} @type miss :: {:miss, :cold} | {:miss, :expired} | {:miss, :request_rejected} | {:miss, :response_rejected} require Logger import Plug.Conn @defaults [ enabled: true, debug: false, store: PlugResponseCache.Stores.Ets, profile: PlugResponseCache.Profiles.Default ] @doc """ While you can pass options to the plug, it's recommended to configure the response cache through the application's config, so the response cache has the necessary processes running. """ def init(opts) do @defaults |> Keyword.merge(Application.get_all_env(:plug_response_cache)) |> Keyword.merge(opts) |> Enum.into(%{}) end def call(conn, %{enabled: false} = opts), do: mark_miss(conn, :disabled, opts) def call(conn, opts) do if opts.profile.cache_request?(conn, opts) do send_cached(conn, opts) else mark_miss(conn, :request_rejected, opts) end end defp send_cached(conn, opts) do case opts.store.get(conn) do {:hit, {status, body, expires}} -> conn |> mark_hit(expires, opts) |> send_resp(status, body) |> halt() {:miss, reason} -> register_before_send(conn, fn conn -> if opts.profile.cache_response?(conn, opts) do cache_response(conn, reason, opts) else mark_miss(conn, :response_rejected, opts) end end) end end defp cache_response(conn, miss_reason, opts) do expires = opts.profile.expires(conn, opts) conn |> opts.store.set(expires) |> mark_miss(miss_reason, opts) end defp mark_miss(conn, reason, %{debug: debug}) do conn |> log(debug, :miss, reason) |> put_private(:plug_response_cache, {:miss, reason}) end defp mark_hit(conn, expires, %{debug: debug}) do conn |> log(debug, :hit, expires) |> put_private(:plug_response_cache, {:hit, expires}) end defp log(conn, false, _result, _reason), do: conn defp log(conn, true, :miss, reason) do Logger.debug("PlugResponseCache {:miss, :" <> Atom.to_string(reason) <> "} " <> conn.request_path) conn end defp log(conn, true, :hit, :never) do Logger.debug("PlugResponseCache {:hit, :never} " <> conn.request_path) conn end defp log(conn, true, :hit, expire_time) do Logger.debug("PlugResponseCache {:hit, " <> DateTime.to_iso8601(expire_time) <> "} " <> conn.request_path) conn end end