summaryrefslogtreecommitdiffstats
path: root/lib/cepis/error.ex
diff options
context:
space:
mode:
Diffstat (limited to 'lib/cepis/error.ex')
-rw-r--r--lib/cepis/error.ex157
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