RFC 9110 entity tags and If-Match / If-None-Match.
Parses entity tags and evaluates preconditions. The library reports whether
a precondition is satisfied; the caller maps that to 304 or 412. It does not
send those statuses or set Cache-Control.
Behaviour
- Build tags with
new/2(opaque octets or an integer such aslock_version),parse/1(a quotedETagfield), orfrom_content/2(hash of canonical iodata you already have — notJason.encode!/1). new("1")is opaque1.parse(~S("1"))is the wire field"1".- If-Match uses strong comparison; If-None-Match uses weak comparison. Do
not use
==on the struct. - A missing header is
niland skips the precondition (:ok).""is an empty list, not a missing header. - Functions return
:okor{:error, exception}. Programmer mistakes (wrongcurrentorheadertype) raiseArgumentError. - Hand-built structs are not validated. A
"inopaqueis not a valid entity-tag.
Examples
iex> {:ok, tag} = HttpEtag.parse(~S("abc"))
iex> HttpEtag.to_header(tag)
~S("abc")
iex> HttpEtag.if_match(HttpEtag.parse!(~S("abc")), ~S("abc"))
:okSee HttpEtag.Conn, HttpEtag.Error, and
RFC 9110 §8.8.3 and §13.1.
Summary
Types
Option for from_content/2.
Keyword options for from_content/2.
A parsed entity-tag.
A parsed If-Match or If-None-Match field: a tag list, or :any for *.
Functions
Builds an entity-tag from representation octets.
Evaluates If-Match (RFC 9110 §13.1.1).
Same as if_match/2 but raises HttpEtag.Error on failure.
Evaluates If-None-Match (RFC 9110 §13.1.2).
Same as if_none_match/2 but raises HttpEtag.Error on failure.
Builds an entity-tag from opaque octets.
Same as new/2 but raises HttpEtag.Error on failure.
Parses a single entity-tag.
Same as parse/1 but raises HttpEtag.Error on failure.
Parses an If-Match or If-None-Match field.
Same as parse_list/1 but raises HttpEtag.Error on failure.
Strong comparison (RFC 9110 §8.8.3.2).
Formats an entity-tag as an ETag field value.
Weak comparison (RFC 9110 §8.8.3.2).
Types
Option for from_content/2.
@type from_content_opts() :: [from_content_opt()]
Keyword options for from_content/2.
A parsed entity-tag.
opaque is the octets inside the quotes. weak is true for a weak tag
(W/"…").
@type tag_list() :: [t()] | :any
A parsed If-Match or If-None-Match field: a tag list, or :any for *.
Functions
@spec from_content(iodata(), from_content_opts()) :: t()
Builds an entity-tag from representation octets.
Hashes content (iodata) with :sha256 by default and encodes the digest as
lowercase hex, which is always valid etagc. The result is a strong tag
unless weak: true.
Pass canonical bytes you already control (a file, a digest input). Do not
hash Jason.encode!/1 of a struct: key order and omitted nils are unstable.
Prefer new/2 with lock_version (or another unique validator) for Ecto
rows. updated_at at second precision can collide.
Unknown option keys raise ArgumentError. A non-boolean :weak or
non-atom :algorithm raises ArgumentError. An atom that is not a
:crypto.hash_algorithm() raises from :crypto.hash/2.
Options
:algorithm- a:crypto.hash_algorithm(), default:sha256:weak- whentrue, mark the tag weak
Examples
iex> tag = HttpEtag.from_content("abc")
iex> tag.weak
false
iex> tag.opaque
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
iex> HttpEtag.from_content(["ab", "c"]) == HttpEtag.from_content("abc")
true
@spec if_match(t() | nil, String.t() | nil) :: :ok | {:error, HttpEtag.Error.t()}
Evaluates If-Match (RFC 9110 §13.1.1).
Uses strong comparison. header nil means the field is absent (no
precondition) and returns :ok. current nil means the resource has no
representation. If-Match: * succeeds only when current is present.
current must be a %HttpEtag{} or nil. header must be a binary or
nil. Any other term raises ArgumentError.
A weak current never strongly matches a tag list. Prefer strong tags for
lost-update protection.
Returns :ok or {:error, exception} with :precondition_failed or
:invalid_header. Does not choose 412; the caller does.
Examples
iex> HttpEtag.if_match(HttpEtag.parse!(~S("abc")), ~S("abc"))
:ok
iex> HttpEtag.if_match(nil, "*")
{:error, %HttpEtag.Error{reason: :precondition_failed}}
Same as if_match/2 but raises HttpEtag.Error on failure.
Examples
iex> HttpEtag.if_match!(HttpEtag.parse!(~S("abc")), ~S("abc"))
:ok
@spec if_none_match(t() | nil, String.t() | nil) :: :ok | {:error, HttpEtag.Error.t()}
Evaluates If-None-Match (RFC 9110 §13.1.2).
Uses weak comparison. header nil returns :ok. If-None-Match: *
succeeds only when current is nil. A tag list fails when any listed tag
weakly matches current. If current is nil, a tag list succeeds.
current must be a %HttpEtag{} or nil. header must be a binary or
nil. Any other term raises ArgumentError.
Returns :ok or {:error, exception} with :precondition_failed or
:invalid_header. The caller maps a failed GET/HEAD to 304 and other methods
to 412.
Examples
iex> HttpEtag.if_none_match(nil, "*")
:ok
iex> HttpEtag.if_none_match(HttpEtag.parse!(~S("abc")), ~S("abc"))
{:error, %HttpEtag.Error{reason: :precondition_failed}}
Same as if_none_match/2 but raises HttpEtag.Error on failure.
Examples
iex> HttpEtag.if_none_match!(nil, "*")
:ok
@spec new(term(), term()) :: {:ok, t()} | {:error, HttpEtag.Error.t()}
Builds an entity-tag from opaque octets.
weak defaults to false (a strong tag). Opaque octets must match RFC 9110
etagc (! / %x23-7E / obs-text). Double quotes and spaces are rejected.
An integer is formatted with Integer.to_string/1 (use user.lock_version,
not parse/1 of the version string).
Examples
iex> HttpEtag.new("abc")
{:ok, %HttpEtag{opaque: "abc", weak: false}}
iex> HttpEtag.new(1)
{:ok, %HttpEtag{opaque: "1", weak: false}}
iex> HttpEtag.new("abc", true)
{:ok, %HttpEtag{opaque: "abc", weak: true}}
iex> HttpEtag.new("a b")
{:error, %HttpEtag.Error{reason: :invalid_etag}}
Same as new/2 but raises HttpEtag.Error on failure.
Examples
iex> HttpEtag.new!("abc")
%HttpEtag{opaque: "abc", weak: false}
iex> HttpEtag.new!(42)
%HttpEtag{opaque: "42", weak: false}
@spec parse(term()) :: {:ok, t()} | {:error, HttpEtag.Error.t()}
Parses a single entity-tag.
Surrounding optional whitespace (SP / HTAB) is ignored. The whole value
after that must be one entity-tag. "W/" is case-sensitive. There is no
backslash unescaping.
Examples
iex> HttpEtag.parse(~S("abc"))
{:ok, %HttpEtag{opaque: "abc", weak: false}}
iex> HttpEtag.parse(~S(W/"abc"))
{:ok, %HttpEtag{opaque: "abc", weak: true}}
iex> HttpEtag.parse(~S(""))
{:ok, %HttpEtag{opaque: "", weak: false}}
Same as parse/1 but raises HttpEtag.Error on failure.
Examples
iex> HttpEtag.parse!(~S("abc"))
%HttpEtag{opaque: "abc", weak: false}
@spec parse_list(term()) :: {:ok, tag_list()} | {:error, HttpEtag.Error.t()}
Parses an If-Match or If-None-Match field.
Returns {:ok, :any} for *, {:ok, tags} for #entity-tag (empty list
elements are ignored), or {:error, exception} when the field is invalid.
* mixed with tags is invalid.
Recipients ignore empty list elements. More than 256 comma-separated
segments is :invalid_header (RFC 9110 §5.6.1.2).
Examples
iex> HttpEtag.parse_list("*")
{:ok, :any}
iex> HttpEtag.parse_list(~S("a", W/"b"))
{:ok, [%HttpEtag{opaque: "a", weak: false}, %HttpEtag{opaque: "b", weak: true}]}
Same as parse_list/1 but raises HttpEtag.Error on failure.
Examples
iex> HttpEtag.parse_list!("*")
:any
Strong comparison (RFC 9110 §8.8.3.2).
Both tags must be strong and their opaque octets equal. A weak tag never
matches. Use this for If-Match, not == on the struct.
Comparison uses == and is not constant-time. Entity tags are validators,
not secrets.
Examples
iex> HttpEtag.strong_match?(HttpEtag.parse!(~S("1")), HttpEtag.parse!(~S("1")))
true
iex> HttpEtag.strong_match?(HttpEtag.parse!(~S(W/"1")), HttpEtag.parse!(~S("1")))
false
Formats an entity-tag as an ETag field value.
Build the struct with new/2, parse/1, or from_content/2. A hand-built
struct whose opaque contains " is not a valid entity-tag.
Examples
iex> HttpEtag.to_header(%HttpEtag{opaque: "abc", weak: false})
~S("abc")
iex> HttpEtag.to_header(%HttpEtag{opaque: "abc", weak: true})
~S(W/"abc")
Weak comparison (RFC 9110 §8.8.3.2).
Opaque octets must be equal; weakness is ignored. Use this for If-None-Match.
Comparison uses == and is not constant-time. Entity tags are validators,
not secrets.
Examples
iex> HttpEtag.weak_match?(HttpEtag.parse!(~S(W/"1")), HttpEtag.parse!(~S("1")))
true