defmodule Cepis.InvalidCEPError do @moduledoc """ The CEP is not 8 digits. Usually produced **without** a request ever being made — the client checks the format before going to the network. It is also what a `400` from the API becomes, carrying the API's own message: the format already passed local validation, so the server knows something this client does not. Retrying does not help. """ defexception [:message] @type t :: %__MODULE__{message: String.t()} end defmodule Cepis.NotFoundError do @moduledoc """ The CEP is well-formed but does not exist in the DNE database. `cep` holds the normalised 8 digits that were looked up. Retrying does not help: the answer is definitive. `faixa` is a `Cepis.Faixa` naming the municipality that CEP *would* have belonged to, or `nil`. It is context for the failure and **not** an address — this is still a not-found, and the CEP is still not in the database. It is `nil` whenever the number falls in a band no municipality owns, which the API signals by omitting the field rather than sending null. """ alias Cepis.Faixa defexception [:message, :cep, :faixa] @type t :: %__MODULE__{message: String.t(), cep: String.t(), faixa: Faixa.t() | nil} end defmodule Cepis.NetworkError do @moduledoc """ The request never completed: DNS, refused connection, timeout, TLS. `reason` is the underlying `:httpc` reason. Produced only after every configured attempt has failed, so retrying immediately is unlikely to help — reschedule instead. """ defexception [:message, :reason] @type t :: %__MODULE__{message: String.t(), reason: term()} end defmodule Cepis.ServerError do @moduledoc """ The API answered 5xx on every attempt. `status` holds the HTTP status seen on the last attempt. Retrying later may help; the client already retried on its own before returning this. """ defexception [:message, :status] @type t :: %__MODULE__{message: String.t(), status: pos_integer()} end defmodule Cepis.RateLimitError do @moduledoc """ The API (or the edge in front of it) answered 429 on every attempt. `retry_after` is what the server asked for, **in milliseconds**, or `nil` if it did not say. The client already honoured that value across the attempts it made; it is exposed here for callers who want to reschedule the work, e.g. with `Process.send_after/3`. """ defexception [:message, :retry_after] @type t :: %__MODULE__{message: String.t(), retry_after: non_neg_integer() | nil} end defmodule Cepis.InvalidResponseError do @moduledoc """ A success status came back with a body that cannot be parsed. In practice this means something between you and the API is answering instead of it — a captive Wi-Fi portal, a corporate proxy, an HTML error page. Useful as a diagnosis: the problem is not in your code. """ defexception [:message] @type t :: %__MODULE__{message: String.t()} end defmodule Cepis.Error do @moduledoc """ The error surface, and how to tell the two kinds apart. Every function in `Cepis` returns `{:ok, result}` or `{:error, error}`, where `error` is one of the exception structs listed in `t:t/0`. Matching on `{:error, error}` is therefore the one handler that catches everything; the bang variants raise the same structs instead. Below that, the split is by **the decision the caller has to make**, not by the HTTP status that caused it: - `Cepis.InvalidCEPError` and `Cepis.NotFoundError` are problems with the *request*. Retrying is pointless — the same CEP will fail again. - `Cepis.NetworkError`, `Cepis.ServerError` and `Cepis.RateLimitError` are problems with *transport*. Retrying may help, and the client already retried on its own before returning any of them. That split is why `Cepis.NotFoundError` is not "the HTTP 404 error": a malformed CEP is rejected before it ever leaves the machine, with no request made at all. """ @exceptions [ Cepis.InvalidCEPError, Cepis.NotFoundError, Cepis.NetworkError, Cepis.ServerError, Cepis.RateLimitError, Cepis.InvalidResponseError ] @type t :: Cepis.InvalidCEPError.t() | Cepis.NotFoundError.t() | Cepis.NetworkError.t() | Cepis.ServerError.t() | Cepis.RateLimitError.t() | Cepis.InvalidResponseError.t() @doc """ Every exception module this library produces. Useful for building a `rescue` clause around the bang variants: rescue error in [Cepis.NotFoundError, Cepis.InvalidCEPError] -> ... end """ @spec exceptions() :: [module()] def exceptions, do: @exceptions @doc """ Whether `term` is one of this library's errors. """ @spec error?(term()) :: boolean() def error?(%module{}), do: module in @exceptions def error?(_), do: false @doc """ Whether retrying the same call later could plausibly succeed. `true` for transport failures (network, 5xx, 429), `false` for definitive answers about the request itself. """ @spec retriable?(t()) :: boolean() def retriable?(%Cepis.NetworkError{}), do: true def retriable?(%Cepis.ServerError{}), do: true def retriable?(%Cepis.RateLimitError{}), do: true def retriable?(_), do: false end