HttpEtag

CIHex.pmHexdocs.pm

RFC 9110 entity tags and If-Match / If-None-Match for Elixir.

Parses ETag / If-Match / If-None-Match and reports whether a precondition is satisfied. You map that onto 304 or 412. This library does not set Cache-Control and is not a replacement for Plug.Static.

iex> {:ok, tag} = HttpEtag.new(1)
iex> HttpEtag.to_header(tag)
~S("1")
iex> HttpEtag.if_none_match(tag, ~S("1"))
{:error, %HttpEtag.Error{reason: :precondition_failed}}

Installation

def deps do
[
{:http_etag, "~> 0.1.0"}
]
end

HttpEtag.Conn is compiled when Plug.Conn is available. Add :plug if it is not already in the project (Phoenix already depends on it). Conn only reads and writes headers; the caller sends 304 or 412.

Minting tags

Use new/2 for an opaque validator. Integers are allowed so Ecto lock_version works directly. Prefer that over updated_at (second precision can collide).

etag = HttpEtag.new!(user.lock_version)
HttpEtag.to_header(etag)
# => "\"1\""

new("1") is opaque 1. parse(~S("1")) is the quoted wire field. Do not parse("#{user.lock_version}").

from_content/2 hashes canonical iodata you already have (file bytes, a digest input). Do not hash Jason.encode!(user): key order and omitted nils are unstable.

HttpEtag.from_content(file_bytes)
HttpEtag.from_content(["prefix", body], algorithm: :sha512)

Weak tags never satisfy If-Match.

GET and HEAD → 304

:ok means send the body. :precondition_failed means the client's tag matches (304). Put ETag on both the 200 and the 304.

etag = HttpEtag.new!(user.lock_version)
case HttpEtag.Conn.if_none_match(conn, etag) do
:ok ->
conn |> HttpEtag.Conn.put_etag(etag) |> json(user)
{:error, %{reason: :precondition_failed}} ->
conn
|> HttpEtag.Conn.put_etag(etag)
|> Plug.Conn.send_resp(304, "")
{:error, %{reason: :invalid_header}} ->
Plug.Conn.send_resp(conn, 400, "")
end

HEAD uses the same if_none_match/2 call.

PATCH, PUT, and DELETE → 412

etag = HttpEtag.new!(user.lock_version)
case HttpEtag.Conn.if_match(conn, etag) do
:ok ->
with {:ok, patched} <- JsonMergePatch.apply_patch(document, patch) do
save(patched)
end
{:error, %{reason: :precondition_failed}} ->
conn
|> HttpEtag.Conn.put_etag(etag)
|> Plug.Conn.send_resp(412, "")
{:error, %{reason: :invalid_header}} ->
Plug.Conn.send_resp(conn, 400, "")
end

json_merge_patch (docs) is a separate library; this package does not depend on it.

Authorize the change in the caller. See RFC 9110 §13.1.

Sponsor

Netoum

Contributing

See CONTRIBUTING.md. This project follows the Code of Conduct.

License

MIT © Netoum. See LICENSE.