HTTP transport for the OpenFeed sharing API.
Handles DPoP, pagination, retries and error classification. Most callers want
OpenFeed.Sharing, which wraps this with a named function per endpoint.
Retries are handled here, not by Req
DPoP proofs are single-use: the server remembers each jti and rejects
reuse. Req's built-in retry replays the same request, headers and all,
which would present an already-spent proof and earn a 401. So retries live
here, and every attempt gets a freshly signed proof — the nonce dance,
transient 5xx retries and transport retries alike.
Pagination follows links.next
Collection responses carry links.next, present only when more records
exist. get_all/4 follows that link.
It does not infer the end from a short page. That heuristic looks
equivalent and is not: with a record count that is an exact multiple of the
page size, a short-page check requests one page too many, and OpenFeed
answers offset >= total with 400 no_records_found_at_offset_limit —
turning a perfectly good full read into a spurious failure. Following
links.next sidesteps the arithmetic entirely.
Response shape
get/4 and delete/3 return the whole envelope
(%{"version" => ..., "data" => ..., "meta" => ..., "links" => ...}).
get_all/4 returns just the concatenated data items.
Summary
Functions
DELETE a resource. Returns {:ok, envelope_or_nil} — 204 yields nil.
GET a single resource or one page of a collection.
GET every page of a collection, following links.next.
OpenFeed's maximum (and default) page size.
Lazily stream every item in a collection, one page at a time.
The authorization scheme to use for a token.
Types
@type opts() :: [ params: keyword() | map(), token_type: String.t(), max_attempts: pos_integer(), receive_timeout: timeout(), sleep_fn: (non_neg_integer() -> any()) ]
Options accepted by every request function.
@type token() :: String.t() | OpenFeed.Tokens.t()
An access token.
Prefer passing the OpenFeed.Tokens struct: it carries the token_type
OpenFeed actually issued, which is the only reliable source of it. A bare
string works, but then the scheme has to be guessed — see token_type/2.
Functions
@spec delete(OpenFeed.Config.t(), token(), String.t(), opts()) :: {:ok, map() | nil} | {:error, OpenFeed.Error.t()}
DELETE a resource. Returns {:ok, envelope_or_nil} — 204 yields nil.
@spec get(OpenFeed.Config.t(), token(), String.t(), opts()) :: {:ok, map()} | {:error, OpenFeed.Error.t()}
GET a single resource or one page of a collection.
Returns the full response envelope.
@spec get_all(OpenFeed.Config.t(), token(), String.t(), opts()) :: {:ok, [term()]} | {:error, OpenFeed.Error.t()}
GET every page of a collection, following links.next.
Returns the concatenated data lists.
@spec max_page_size() :: pos_integer()
OpenFeed's maximum (and default) page size.
@spec stream(OpenFeed.Config.t(), token(), String.t(), opts()) :: Enumerable.t()
Lazily stream every item in a collection, one page at a time.
Use this instead of get_all/4 when a collection could be large — a year of
transactions across several accounts does not want to be a single list in
memory.
Unlike the other functions here, this raises OpenFeed.Error on failure
rather than returning a tuple, because a lazy stream has nowhere to put an
error tuple. Wrap it if you need the tuple back:
try do
OpenFeed.Client.stream(config, token, path) |> Enum.take(50)
rescue
error in OpenFeed.Error -> {:error, error}
end
@spec token_type(OpenFeed.Config.t(), token()) :: String.t()
The authorization scheme to use for a token.
Taken from the token itself when an OpenFeed.Tokens is given. For a bare
string it falls back to the client profile's usual scheme, which is a guess
— and a wrong one for client-credentials tokens, which OpenFeed issues as
Bearer even to a private_key_jwt client. Pass the struct, or an explicit
:token_type, whenever the type is knowable.