Parsing and validation of the Prefer request header for the read path
(the write path's Prefer handling lives in Bier.Mutation).
Mirrors PostgREST v16.0's ApiRequest.Preferences semantics:
- a preference is recognized by its WHOLE token, not by its key:
acceptedPrefsis the list of canonical<key>=<value>strings theToHeaderValueinstances produce, andisUnacceptabletests membership in it (Preferences.hs#L145-L163). Onlytimezone=andmax-affected=, whose values are free-form, are matched by prefix. A token with a known key but an unknown value (count=none,return=bogus) is therefore invalid, not merely ignored; handling=strictrejects the whole request (400PGRST122) when ANY supplied preference is invalid. The errordetailslists the offending tokens verbatim, comma-separated;handling=lenient(or no handling) silently ignores invalid preferences;timezone=<value>is never an invalid preference. v16.0 dropped thepg_timezone_namesmembership test that v14.12 applied (fromHeaderslost itsTimezoneNamesargument along withisTimezonePrefAccepted,Preferences.hs#L129-L163), so every value is accepted as a preference and handed to PostgreSQL as the sessionTimeZone. A value PostgreSQL rejects therefore surfaces as an ordinary database error (SQLSTATE22023-> 400), not as aPGRST122— andhandlinghas nothing left to suppress, which is why the docs say "handling=lenient is ignored for timezone. Invalid time zones always return an error";- the applied preferences are echoed in
Preference-Appliedin PostgREST's canonicalprefsValsorder — resolution, missing, representation, count, transaction, handling, timezone, max-affected (Preferences.hs#L179-L188) — never in request order. On a read onlycount,handlingandtimezonecan apply, so the echo iscountbeforehandlingbeforetimezone.
Numeric UTC offsets (+05:30, -4) are consequently valid too: they are not
members of pg_timezone_names but PostgreSQL accepts them as a TimeZone,
and the echo carries the raw preference token rather than a normalized zone
name.
Summary
Types
The Prefer: count= mode a request resolves to.
Functions
The Prefer: count= mode the request asks for, or :none when it asks for
none (PostgREST's preferCount = Nothing default).
Parse the connection's Prefer header for a read.
Put the Preference-Applied echo (from parse_read/1's :applied list) on a
response, omitting the header entirely when nothing applied.
Types
Functions
@spec count_mode(Plug.Conn.t()) :: count_mode()
The Prefer: count= mode the request asks for, or :none when it asks for
none (PostgREST's preferCount = Nothing default).
parsePrefs walks the REQUEST tokens and returns the first that is a known
count token (Preferences.hs#L165-L167), so with several present the earliest
in the header wins — request order, not the order of the internal constructor
list (Preferences.hs#L98-L101).
Parse the connection's Prefer header for a read.
Returns:
{:ok, %{timezone: tz | nil, handling: :strict | :lenient | nil, max_affected: integer | nil, applied: [token]}}— the timezone to hand to PostgreSQL (nil when none was requested), thehandlingandmax-affectedpreferences the caller may need to enforce, and the tokens to echo inPreference-Applied.{:error, {:invalid_prefs, details}}—handling=strictwith one or more invalid preferences;detailsis the"Invalid preferences: a, b"string.
handling/max_affected are reported but not echoed here beyond
handling=: max-affected constrains how many rows a statement may affect,
which only a plan that affects rows can honor, so it is the caller's job to
enforce it (Bier.Rpc rejects it outright on a routine that cannot return a
set, Bier.Mutation counts the affected rows).
@spec put_applied(Plug.Conn.t(), [String.t()]) :: Plug.Conn.t()
Put the Preference-Applied echo (from parse_read/1's :applied list) on a
response, omitting the header entirely when nothing applied.
Both the relation-read and the /rpc/ paths call this: responsePreferences
masks the mutation-only preferences per plan but passes preferCount (and
handling/timezone) through untouched for EVERY plan (Response.hs#L296), and
the RPC dispatch builds its prefHeader from that same rewritten set
(Response.hs#L184). One rule, one call site.