diff options
Diffstat (limited to 'lib/cepis/error.ex')
| -rw-r--r-- | lib/cepis/error.ex | 157 |
1 files changed, 157 insertions, 0 deletions
diff --git a/lib/cepis/error.ex b/lib/cepis/error.ex new file mode 100644 index 0000000..bd86b65 --- /dev/null +++ b/lib/cepis/error.ex @@ -0,0 +1,157 @@ +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. + """ + + defexception [:message, :cep] + + @type t :: %__MODULE__{message: String.t(), cep: String.t()} +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 |