Packages

SDK oficial Elixir da plataforma APIBrasil: WhatsApp, SMS, consultas de CPF/CNPJ, veiculos, CEP, correios, pagamentos PIX/boleto e mais.

Current section

Files

Jump to
apibrasil lib api_brasil core error.ex
Raw

lib/api_brasil/core/error.ex

defmodule ApiBrasil.Core.Error do
@moduledoc """
Erro devolvido por todas as chamadas da SDK.
Toda função devolve `{:ok, resultado}` ou `{:error, %ApiBrasil.Core.Error{}}`,
e a falha carrega a categoria em `:kind` — o equivalente Elixir às
subclasses de erro das demais SDKs da plataforma.
case ApiBrasil.Data.Consulta.cpf(client, %{"cpf" => "00000000000"}) do
{:ok, consulta} ->
ApiBrasil.Core.CreditResponse.data(consulta)
{:error, %ApiBrasil.Core.Error{kind: :insufficient_balance}} ->
IO.puts("Recarregue seus créditos")
{:error, %ApiBrasil.Core.Error{kind: :rate_limit} = error} ->
IO.puts("Aguarde \#{error.retry_after}ms")
{:error, error} ->
IO.puts(Exception.message(error))
end
As variantes `!` (ex: `cpf!/3`) levantam este erro em vez de devolver
a tupla.
"""
@typedoc """
Categoria da falha.
- `:api` — falha genérica da API;
- `:network` — falha de rede, a requisição pode não ter chegado ao servidor;
- `:timeout` — tempo limite excedido (nunca refeito automaticamente);
- `:validation` — HTTP 400/422, payload inválido;
- `:authentication` — HTTP 401, Bearer Token ausente, inválido ou expirado;
- `:insufficient_balance` — HTTP 402, saldo/créditos insuficientes;
- `:permission` — HTTP 403, sem permissão (ex: API exige conta PJ);
- `:not_found` — HTTP 404/410, recurso não encontrado ou desativado;
- `:rate_limit` — HTTP 429, limite de requisições atingido;
- `:server` — HTTP 5xx, erro interno do gateway/provedor.
"""
@type kind ::
:api
| :network
| :timeout
| :validation
| :authentication
| :insufficient_balance
| :permission
| :not_found
| :rate_limit
| :server
@type t :: %__MODULE__{
kind: kind(),
message: String.t(),
status: pos_integer() | nil,
code: String.t() | nil,
response: term(),
retry_after: non_neg_integer() | nil,
reason: term()
}
defexception kind: :api,
message: "Falha na chamada à API.",
status: nil,
code: nil,
response: nil,
retry_after: nil,
reason: nil
@kinds [
:api,
:network,
:timeout,
:validation,
:authentication,
:insufficient_balance,
:permission,
:not_found,
:rate_limit,
:server
]
@doc "Lista todas as categorias de erro."
@spec kinds() :: [kind()]
def kinds, do: @kinds
@impl true
def message(%__MODULE__{} = error) do
[
error.message,
error.status && " (HTTP #{error.status})",
error.code && " [#{error.code}]"
]
|> Enum.reject(&(&1 in [nil, false]))
|> IO.iodata_to_binary()
end
@doc "Cria um erro da categoria informada."
@spec new(kind(), String.t(), keyword()) :: t()
def new(kind, message, fields \\ []) when kind in @kinds do
struct!(%__MODULE__{kind: kind, message: message}, fields)
end
@doc "Falha de rede — nenhuma resposta recebida."
@spec network(String.t(), keyword()) :: t()
def network(message, fields \\ []), do: new(:network, message, fields)
@doc "Falha por tempo limite excedido."
@spec timeout(String.t(), keyword()) :: t()
def timeout(message, fields \\ []), do: new(:timeout, message, fields)
@doc "Falha de validação (payload inválido)."
@spec validation(String.t(), keyword()) :: t()
def validation(message, fields \\ []), do: new(:validation, message, fields)
@doc "Falha de autenticação."
@spec authentication(String.t(), keyword()) :: t()
def authentication(message, fields \\ []), do: new(:authentication, message, fields)
@doc "Falha antes da resposta — rede ou tempo limite."
@spec network?(t()) :: boolean()
def network?(%__MODULE__{kind: kind}), do: kind in [:network, :timeout]
@doc "Tempo limite excedido."
@spec timeout?(t()) :: boolean()
def timeout?(%__MODULE__{kind: kind}), do: kind == :timeout
@doc "HTTP 400/422 — payload inválido."
@spec validation?(t()) :: boolean()
def validation?(%__MODULE__{kind: kind}), do: kind == :validation
@doc "HTTP 401 — token ausente, inválido ou expirado."
@spec authentication?(t()) :: boolean()
def authentication?(%__MODULE__{kind: kind}), do: kind == :authentication
@doc "HTTP 402 — saldo/créditos insuficientes."
@spec insufficient_balance?(t()) :: boolean()
def insufficient_balance?(%__MODULE__{kind: kind}), do: kind == :insufficient_balance
@doc "HTTP 403 — sem permissão."
@spec permission?(t()) :: boolean()
def permission?(%__MODULE__{kind: kind}), do: kind == :permission
@doc "HTTP 404/410 — não encontrado ou desativado."
@spec not_found?(t()) :: boolean()
def not_found?(%__MODULE__{kind: kind}), do: kind == :not_found
@doc "HTTP 429 — rate limit atingido."
@spec rate_limit?(t()) :: boolean()
def rate_limit?(%__MODULE__{kind: kind}), do: kind == :rate_limit
@doc "HTTP 5xx — erro interno do gateway/provedor."
@spec server?(t()) :: boolean()
def server?(%__MODULE__{kind: kind}), do: kind == :server
@doc "Falha genérica da API."
@spec api?(t()) :: boolean()
def api?(%__MODULE__{kind: kind}), do: kind == :api
@doc """
Mapeia um status HTTP + corpo de erro para o erro adequado, replicando a
hierarquia de erros das demais SDKs.
"""
@spec from_api(pos_integer(), term(), map()) :: t()
def from_api(status, body, headers \\ %{}) do
kind = kind_for_status(status)
retry_after =
if kind == :rate_limit do
parse_retry_after(headers)
end
%__MODULE__{
kind: kind,
message: extract_message(status, body),
status: status,
code: extract_code(body),
response: body,
retry_after: retry_after
}
end
@doc "Categoria correspondente a um status HTTP."
@spec kind_for_status(pos_integer()) :: kind()
def kind_for_status(status) do
cond do
status in [400, 422] -> :validation
status == 401 -> :authentication
status == 402 -> :insufficient_balance
status == 403 -> :permission
status in [404, 410] -> :not_found
status == 429 -> :rate_limit
status >= 500 -> :server
true -> :api
end
end
@doc """
Lê o header `Retry-After` — segundos ou data HTTP (IMF-fixdate) — e
devolve a espera em milissegundos.
"""
@spec parse_retry_after(map(), integer() | nil) :: non_neg_integer() | nil
def parse_retry_after(headers, now \\ nil) do
with raw when is_binary(raw) <- header(headers, "retry-after"),
raw = String.trim(raw),
false <- raw == "" do
parse_retry_after_value(raw, now || System.system_time(:second))
else
_ -> nil
end
end
@doc "Lê um header sem diferenciar maiúsculas de minúsculas."
@spec header(map(), String.t()) :: String.t() | nil
def header(headers, name) when is_map(headers) do
wanted = String.downcase(name)
Enum.find_value(headers, fn {key, value} ->
String.downcase(to_string(key)) == wanted && value
end)
end
def header(_headers, _name), do: nil
defp parse_retry_after_value(raw, now) do
case Float.parse(raw) do
{seconds, ""} when seconds > 0 -> round(seconds * 1000)
{_seconds, ""} -> nil
_ -> parse_http_date(raw, now)
end
end
defp parse_http_date(raw, now) do
case :httpd_util.convert_request_date(String.to_charlist(raw)) do
:bad_date ->
nil
datetime ->
# 62_167_219_200 = segundos entre o ano 0 e a época Unix.
at = :calendar.datetime_to_gregorian_seconds(datetime) - 62_167_219_200
case at - now do
delta when delta > 0 -> delta * 1000
_ -> nil
end
end
end
defp extract_message(status, body) when is_map(body) do
Enum.find_value(["message", "error"], fn key ->
case Map.get(body, key) do
text when is_binary(text) and text != "" -> text
_ -> nil
end
end) || default_message(status)
end
defp extract_message(status, _body), do: default_message(status)
defp default_message(status), do: "A API respondeu com HTTP #{status}."
defp extract_code(body) when is_map(body) do
case Map.get(body, "code") do
code when is_binary(code) -> code
_ -> nil
end
end
defp extract_code(_body), do: nil
end