Packages

A simple to use plug for shallow ETags

Current section

Files

Jump to
etag_plug lib etag plug.ex
Raw

lib/etag/plug.ex

defmodule ETag.Plug do
alias ETag.Plug.Options
@moduledoc """
A drop in plug to add support for shallow [ETags](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag).
Shallow means that it uses the whole response to generate the ETag and does
not care about the specific content of each response. It is not context
sensitive.
For a deep (speak context sensitive) generation of ETags you can take a look
at [Phoenix ETag](https://github.com/michalmuskala/phoenix_etag).
# Usage
You can simply use the plug without any configuration, it then defaults to the
configuration as specified in the "Configuration" section.
plug ETag.Plug
You can also provide a number of options, see the "Configuration" section for details.
plug ETag.Plug,
generator: MyCustomGenerator,
methods: ["GET", "HEAD"],
status_codes: [:ok, 201, :not_modified]
# Configuration
## `generator`
Expects a module implementing the `ETag.Generator` behaviour. The plug ships
with a number of "default" generators:
- `ETag.Generator.MD5`
- `ETag.Generator.SHA1`
- `ETag.Generator.SHA512`
### Default
iex> Application.get_env(:etag_plug, :generator, #{inspect(Options.default(:generator))})
#{inspect(Options.default(:generator))}
## `methods`
Expects a list of strings, describing the HTTP methods for which ETags
should be generated and evaluated.
### Default
iex> Application.get_env(:etag_plug, :methods, #{inspect(Options.default(:methods))})
#{inspect(Options.default(:methods))}
## `status_codes`
Expects an enumerable of integers (or status atoms) which define the statuses
for which ETags should be handled and generated.
### Default
iex> Application.get_env(:etag_plug, :status_codes, #{inspect(Options.default(:status_codes))})
#{inspect(Options.default(:status_codes))}
"""
import Plug.Conn,
only: [
register_before_send: 2,
resp: 3
]
require Logger
defdelegate init(opts), to: Options, as: :sanitize!
def call(conn, opts) do
register_before_send(conn, &handle_etag(&1, opts))
end
def handle_etag(conn, opts) do
if should_handle_etag?(conn, opts) do
do_handle_etag(conn, opts)
else
conn
end
end
defp should_handle_etag?(conn, opts) do
relevant_method?(conn, opts) and relevant_status?(conn, opts)
end
defp relevant_method?(%Plug.Conn{method: method}, opts) do
String.upcase(method) in Keyword.fetch!(opts, :methods)
end
defp relevant_status?(%Plug.Conn{status: status}, opts) do
Plug.Conn.Status.code(status) in Keyword.fetch!(opts, :status_codes)
end
defp do_handle_etag(conn, opts) do
case generate_etag(conn.resp_body, opts) do
nil ->
conn
etag ->
conn
|> ETag.put(etag)
|> respond_304_if_not_modified(etag)
end
end
defp generate_etag(content, opts) do
opts
|> Keyword.fetch!(:generator)
|> apply(:generate, [content])
end
defp respond_304_if_not_modified(conn, etag) do
conn
|> ETag.match?(etag)
|> if do
resp(conn, 304, "")
else
conn
end
end
end