Packages
tokenio_client
1.0.0
Production-grade Elixir client for the Token.io Open Banking platform (Payments v2, VRP, AIS, Banks, Refunds, Payouts, Settlement, Transfers, Tokens, Token Requests, Account on File, Sub-TPPs, Auth Keys, Reports, Webhooks, Verification).
Current section
Files
Jump to
Current section
Files
lib/tokenio/error.ex
defmodule TokenioClient.Error do
@moduledoc """
Typed error returned by all Token.io API functions.
## Error codes
| Code | HTTP status |
|---|---|
| `:unauthorized` | 401 |
| `:forbidden` | 403 |
| `:not_found` | 404 |
| `:conflict` | 409 |
| `:validation_error` | 400 / 422 |
| `:rate_limit_exceeded` | 429 |
| `:internal_server_error` | 500 |
| `:bad_gateway` | 502 |
| `:service_unavailable` | 503 |
| `:timeout` | 504 |
| `:unknown` | other |
## Pattern matching
case TokenioClient.Payments.get(client, id) do
{:ok, payment} -> payment
{:error, %TokenioClient.Error{code: :not_found}} -> nil
{:error, %TokenioClient.Error{code: :rate_limit_exceeded, retry_after: ra}} ->
Process.sleep((ra || 5) * 1_000)
{:error, err} -> raise err
end
"""
@type code ::
:unauthorized
| :forbidden
| :not_found
| :conflict
| :validation_error
| :rate_limit_exceeded
| :internal_server_error
| :bad_gateway
| :service_unavailable
| :timeout
| :unknown
@type t :: %__MODULE__{
code: code(),
message: String.t(),
status: non_neg_integer(),
request_id: String.t() | nil,
details: map() | nil,
retry_after: non_neg_integer() | nil
}
defexception [:code, :message, :status, :request_id, :details, :retry_after]
@spec message(t()) :: String.t()
@impl Exception
def message(%__MODULE__{code: code, message: msg, status: status, request_id: rid}) do
base = "[#{code}] #{msg} (HTTP #{status})"
if rid, do: "#{base} trace_id=#{rid}", else: base
end
@doc "Returns `true` if the error is retryable (429, 500, 502, 503, 504)."
@spec retryable?(t()) :: boolean()
def retryable?(%__MODULE__{status: s}) when s in [429, 500, 502, 503, 504], do: true
def retryable?(%__MODULE__{}), do: false
@doc "Returns `true` if the error is a 404 Not Found."
@spec not_found?(t()) :: boolean()
def not_found?(%__MODULE__{code: :not_found}), do: true
def not_found?(%__MODULE__{}), do: false
@doc "Returns `true` if the error is a 401 Unauthorized."
@spec unauthorized?(t()) :: boolean()
def unauthorized?(%__MODULE__{code: :unauthorized}), do: true
def unauthorized?(%__MODULE__{}), do: false
@doc "Returns `true` if the error is a 429 Rate Limit Exceeded."
@spec rate_limited?(t()) :: boolean()
def rate_limited?(%__MODULE__{code: :rate_limit_exceeded}), do: true
def rate_limited?(%__MODULE__{}), do: false
# ---------------------------------------------------------------------------
# Internal constructors (not part of public API)
# ---------------------------------------------------------------------------
@doc false
@spec from_response(non_neg_integer(), map(), String.t() | nil, String.t() | nil) :: t()
def from_response(status, body, request_id, retry_after_header) do
raw_code = Map.get(body, "code", "")
msg = Map.get(body, "message") || default_message(status)
details = Map.get(body, "details")
retry_after =
case Integer.parse(retry_after_header || "") do
{n, ""} -> n
_ -> nil
end
%__MODULE__{
code: resolve_code(status, raw_code),
message: msg,
status: status,
request_id: request_id,
details: details,
retry_after: retry_after
}
end
@doc false
@spec network_error(term()) :: t()
def network_error(reason) do
%__MODULE__{
code: :unknown,
message: "Network error: #{inspect(reason)}",
status: 0,
request_id: nil,
details: nil,
retry_after: nil
}
end
@doc false
@spec validation(String.t()) :: t()
def validation(msg) do
%__MODULE__{code: :unknown, message: msg, status: 0}
end
# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
@code_map %{
"UNAUTHORIZED" => :unauthorized,
"FORBIDDEN" => :forbidden,
"NOT_FOUND" => :not_found,
"CONFLICT" => :conflict,
"VALIDATION_ERROR" => :validation_error,
"RATE_LIMIT_EXCEEDED" => :rate_limit_exceeded,
"INTERNAL_SERVER_ERROR" => :internal_server_error,
"SERVICE_UNAVAILABLE" => :service_unavailable,
"TIMEOUT" => :timeout,
"BAD_GATEWAY" => :bad_gateway,
"DEADLINE_EXCEEDED" => :timeout
}
@status_map %{
401 => :unauthorized,
403 => :forbidden,
404 => :not_found,
409 => :conflict,
400 => :validation_error,
422 => :validation_error,
429 => :rate_limit_exceeded,
500 => :internal_server_error,
502 => :bad_gateway,
503 => :service_unavailable,
504 => :timeout
}
defp resolve_code(_status, raw) when raw != "" do
Map.get(@code_map, raw, :unknown)
end
defp resolve_code(status, _raw) do
Map.get(@status_map, status, :unknown)
end
defp default_message(status) do
case status do
401 -> "Unauthorized"
403 -> "Forbidden"
404 -> "Not found"
409 -> "Conflict"
422 -> "Unprocessable entity"
429 -> "Rate limit exceeded"
500 -> "Internal server error"
502 -> "Bad gateway"
503 -> "Service unavailable"
504 -> "Gateway timeout"
_ -> "Unknown error (HTTP #{status})"
end
end
end