FoPost.Error exception (FoPost v0.1.0)

Copy Markdown View Source

The single error type every FoPost call answers with.

The API replies to a failed request with {"error": "<code>", "message": "<text>"}, which maps onto :code and :message. :body keeps the decoded body as it arrived so fields this struct does not model stay reachable.

Elixir does not need one exception module per status code — the status is on the struct and the predicates below cover the cases callers actually branch on:

case FoPost.Posts.publish(client, post.id) do
  {:ok, result} ->
    result

  {:error, %FoPost.Error{} = error} ->
    cond do
      FoPost.Error.rate_limited?(error) -> retry_in(error.retry_after)
      FoPost.Error.payment_required?(error) -> send_to(error.upgrade_url)
      true -> Logger.error(Exception.message(error))
    end
end

A transport failure — connection refused, DNS, a timeout — is the same struct with status: nil and code: "transport_error", so a caller never has to match on two shapes.

The struct is also an exception, so raise error and the bang variants of every resource function work the way they do elsewhere in Elixir.

Summary

Functions

True for a 403 — the key is valid but lacks the scope or the workspace.

True for a 404 — no such resource, or it sits outside this key's reach.

True for a 402 — no active subscription, or AI credits are exhausted.

True for a 429. :retry_after holds the wait the API asked for, in seconds.

True for any 5xx.

True when the request never produced a response: connection refused, DNS, a timeout.

True for a 401 — the API key is missing, malformed, or revoked.

True for a 400 or 422 — the request body did not validate.

Types

t()

@type t() :: %FoPost.Error{
  __exception__: true,
  body: term(),
  code: String.t() | nil,
  message: String.t() | nil,
  retry_after: pos_integer() | nil,
  status: pos_integer() | nil,
  upgrade_url: String.t() | nil
}

Functions

forbidden?(error)

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

True for a 403 — the key is valid but lacks the scope or the workspace.

not_found?(error)

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

True for a 404 — no such resource, or it sits outside this key's reach.

payment_required?(error)

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

True for a 402 — no active subscription, or AI credits are exhausted.

:upgrade_url carries where to send the user when the API supplies one.

rate_limited?(error)

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

True for a 429. :retry_after holds the wait the API asked for, in seconds.

The client already retries a 429 twice on its own, so seeing this means the retries were used up.

server_error?(error)

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

True for any 5xx.

transport_error?(error)

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

True when the request never produced a response: connection refused, DNS, a timeout.

unauthorized?(error)

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

True for a 401 — the API key is missing, malformed, or revoked.

validation?(error)

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

True for a 400 or 422 — the request body did not validate.