HttpEtag
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.
Related
json_merge_patch— RFC 7396 merge patch for a PATCH bodyplug— optionalHttpEtag.Conn.Plug.Staticserves files and compares If-None-Match withetag in get_req_header(conn, "if-none-match")(raw field-line membership). That misses comma lists,W/"…", and*. Use this library for resource If-Match / If-None-Match.plug_http_validator— sets ETag / Last-Modified on responses from structs; does not parse If-Match / If-None-Match
Sponsor
Contributing
See CONTRIBUTING.md. This project follows the Code of Conduct.
License
MIT © Netoum. See LICENSE.