OpenFeed.Error exception (OpenFeed v0.1.0)

Copy Markdown View Source

A failed OpenFeed API call.

Match on :kind, not on :code

:code is the raw machine-readable string OpenFeed returned. :kind is this library's classification of it, and is what you should branch on — it is stable across wording changes upstream, and it collapses the cases where two different statuses mean the same thing to a caller.

case OpenFeed.Sharing.banking_accounts(config, token) do
  {:ok, accounts} ->
    accounts

  {:error, %OpenFeed.Error{kind: :grant_revoked}} ->
    # The consumer withdrew consent. Stop syncing; ask them to reconnect.

  {:error, %OpenFeed.Error{kind: :credit_exhausted}} ->
    # Your OpenFeed credit ran out. Nothing to do with this consumer.

  {:error, %OpenFeed.Error{kind: kind}} when kind in [:server_error, :transport_error] ->
    # Transient. Try again later.
end

Why this distinction matters

OpenFeed returns HTTP 403 for two unrelated situations: disclosure_grant_required (the grant is gone or inactive — the consumer revoked consent) and subject_mismatch (the token's sub does not match the grant's user — a bug on your side, or a token mix-up).

Treating both as "revoked" will mark healthy grants dead. So the two get distinct kinds, :grant_revoked and :subject_mismatch, and only the first means stop syncing.

Kinds

KindTypicallyRetry?
:grant_revoked403 disclosure_grant_requiredNo — needs re-consent
:credit_exhausted402 credit_exhaustedNo — needs a top-up
:subject_mismatch403 subject_mismatchNo — a bug
:forbidden403 otherwiseNo
:unauthorized401No — refresh the token first
:not_found404No
:offset_out_of_range400 no_records_found_at_offset_limitNo
:invalid_request400 otherwiseNo
:balance_unavailable502 balance_temporarily_unavailableYes
:server_error5xxYes
:transport_errorconnection failure, timeoutYes
:unexpectedanything unclassifiedNo

Summary

Functions

Whether OpenFeed metering credit is exhausted for this grant.

Build an error from an HTTP response.

Build an error from a transport-level failure (no HTTP response at all).

Whether this means the grant is dead and no amount of retrying will help.

Build an error that did not come from HTTP at all — a missing key, say.

Whether retrying the same request could plausibly succeed.

Types

kind()

@type kind() ::
  :grant_revoked
  | :credit_exhausted
  | :subject_mismatch
  | :forbidden
  | :unauthorized
  | :not_found
  | :offset_out_of_range
  | :invalid_request
  | :balance_unavailable
  | :server_error
  | :transport_error
  | :unexpected

t()

@type t() :: %OpenFeed.Error{
  __exception__: term(),
  body: term(),
  code: String.t() | nil,
  kind: kind(),
  message: String.t(),
  path: String.t() | nil,
  reason: term(),
  status: pos_integer() | nil
}

Functions

credit_exhausted?(error)

@spec credit_exhausted?(t()) :: boolean()

Whether OpenFeed metering credit is exhausted for this grant.

from_response(status, body, path \\ nil)

@spec from_response(pos_integer(), term(), String.t() | nil) :: t()

Build an error from an HTTP response.

The body is expected to be OpenFeed's {"code": ..., "message": ...} shape, but nothing depends on it being well formed — an HTML error page from a proxy still classifies by status.

from_transport(reason, path \\ nil)

@spec from_transport(term(), String.t() | nil) :: t()

Build an error from a transport-level failure (no HTTP response at all).

grant_revoked?(error)

@spec grant_revoked?(t()) :: boolean()

Whether this means the grant is dead and no amount of retrying will help.

True only for :grant_revoked. Deliberately not true for :subject_mismatch, which is also a 403 but indicates a bug rather than a withdrawn consent.

new(kind, message, opts \\ [])

@spec new(kind(), String.t(), keyword()) :: t()

Build an error that did not come from HTTP at all — a missing key, say.

retryable?(error)

@spec retryable?(t()) :: boolean()

Whether retrying the same request could plausibly succeed.

True for server and transport errors, and for :balance_unavailable — which means OpenFeed could not reach the upstream data holder, not that anything is wrong with your request.