Packages
mercury_client
1.0.0
A complete, production-grade Elixir client for the Mercury Banking API (accounts, transactions, recipients, invoices, payments, treasury, webhooks, and more), with typed errors, retry with backoff, and lazy auto-paginating streams.
Current section
Files
Jump to
Current section
Files
lib/mercury/errors.ex
defmodule Mercury.APIError do
@moduledoc """
The base exception for Mercury HTTP API errors that don't map to a more
specific exception (e.g. HTTP 403 Forbidden, or any unrecognised status).
"""
@type t :: %__MODULE__{
message: String.t(),
code: atom(),
status: pos_integer() | nil,
request_id: String.t() | nil,
body: map() | nil
}
defexception [:message, :code, :status, :request_id, :body]
@impl true
def message(%{message: message, status: status, request_id: nil}) do
"mercury: #{message} (status=#{inspect(status)})"
end
def message(%{message: message, status: status, request_id: request_id}) do
"mercury: #{message} (status=#{inspect(status)}, request_id=#{request_id})"
end
end
defmodule Mercury.AuthenticationError do
@moduledoc "Raised for HTTP 401 Unauthorized responses — the API key is missing or invalid."
@type t :: %__MODULE__{
message: String.t(),
code: atom(),
status: pos_integer(),
request_id: String.t() | nil,
body: map() | nil
}
defexception [:message, :code, :status, :request_id, :body]
@impl true
def message(%{message: message}), do: "mercury: " <> message
end
defmodule Mercury.NotFoundError do
@moduledoc "Raised for HTTP 404 Not Found responses."
@type t :: %__MODULE__{
message: String.t(),
code: atom(),
status: pos_integer(),
request_id: String.t() | nil,
body: map() | nil,
resource: String.t() | nil
}
defexception [:message, :code, :status, :request_id, :body, :resource]
@impl true
def message(%{resource: resource}) when is_binary(resource) do
"mercury: not found: #{resource}"
end
def message(%{message: message}), do: "mercury: not found: #{message}"
end
defmodule Mercury.ValidationError do
@moduledoc "Raised for HTTP 400 Bad Request responses."
@type t :: %__MODULE__{
message: String.t(),
code: atom(),
status: pos_integer(),
request_id: String.t() | nil,
body: map() | nil
}
defexception [:message, :code, :status, :request_id, :body]
@impl true
def message(%{message: message}), do: "mercury: validation error: #{message}"
end
defmodule Mercury.ConflictError do
@moduledoc """
Raised for HTTP 409 Conflict responses (e.g. a reused `idempotencyKey`).
"""
@type t :: %__MODULE__{
message: String.t(),
code: atom(),
status: pos_integer(),
request_id: String.t() | nil,
body: map() | nil
}
defexception [:message, :code, :status, :request_id, :body]
@impl true
def message(%{message: message}), do: "mercury: conflict: #{message}"
end
defmodule Mercury.RateLimitError do
@moduledoc "Raised for HTTP 429 Too Many Requests responses."
@type t :: %__MODULE__{
message: String.t(),
code: atom(),
status: pos_integer(),
request_id: String.t() | nil,
body: map() | nil,
retry_after: non_neg_integer() | nil
}
defexception [:message, :code, :status, :request_id, :body, :retry_after]
@impl true
def message(%{retry_after: retry_after}) when is_integer(retry_after) and retry_after > 0 do
"mercury: rate limited — retry after #{retry_after}s"
end
def message(_error), do: "mercury: rate limited"
end
defmodule Mercury.ServerError do
@moduledoc "Raised for HTTP 5xx Server Error responses. Automatically retried."
@type t :: %__MODULE__{
message: String.t(),
code: atom(),
status: pos_integer(),
request_id: String.t() | nil,
body: map() | nil
}
defexception [:message, :code, :status, :request_id, :body]
@impl true
def message(%{status: status, message: message}) do
"mercury: server error #{status}: #{message}"
end
end
defmodule Mercury.NetworkError do
@moduledoc "Wraps a low-level transport failure (connection refused, DNS failure, etc)."
@type t :: %__MODULE__{message: String.t(), reason: term()}
defexception [:message, :reason]
@impl true
def message(%{message: message}), do: "mercury: network error: #{message}"
end
defmodule Mercury.TimeoutError do
@moduledoc "Raised when a request exceeds the client's configured timeout."
@type t :: %__MODULE__{timeout_ms: pos_integer()}
defexception [:timeout_ms]
@impl true
def message(%{timeout_ms: timeout_ms}), do: "mercury: request timed out after #{timeout_ms}ms"
end
defmodule Mercury.Error do
@moduledoc """
Classification helpers for Mercury exceptions — mirrors `errors.As` style
checks from the reference Go SDK, adapted to pattern matching.
## Examples
case Mercury.Accounts.get(client, id) do
{:ok, account} ->
account
{:error, error} ->
if Mercury.Error.rate_limit_error?(error) do
:timer.sleep(1_000)
Mercury.Accounts.get(client, id)
else
{:error, error}
end
end
"""
@doc "Returns `true` if `error` is a `Mercury.AuthenticationError`."
@spec auth_error?(error :: term()) :: boolean()
def auth_error?(%Mercury.AuthenticationError{}), do: true
def auth_error?(_error), do: false
@doc "Returns `true` if `error` is a `Mercury.NotFoundError`."
@spec not_found_error?(error :: term()) :: boolean()
def not_found_error?(%Mercury.NotFoundError{}), do: true
def not_found_error?(_error), do: false
@doc "Returns `true` if `error` is a `Mercury.ValidationError`."
@spec validation_error?(error :: term()) :: boolean()
def validation_error?(%Mercury.ValidationError{}), do: true
def validation_error?(_error), do: false
@doc "Returns `true` if `error` is a `Mercury.ConflictError`."
@spec conflict_error?(error :: term()) :: boolean()
def conflict_error?(%Mercury.ConflictError{}), do: true
def conflict_error?(_error), do: false
@doc "Returns `true` if `error` is a `Mercury.RateLimitError`."
@spec rate_limit_error?(error :: term()) :: boolean()
def rate_limit_error?(%Mercury.RateLimitError{}), do: true
def rate_limit_error?(_error), do: false
@doc "Returns `true` if `error` is a `Mercury.ServerError`."
@spec server_error?(error :: term()) :: boolean()
def server_error?(%Mercury.ServerError{}), do: true
def server_error?(_error), do: false
@doc "Returns `true` if `error` is a `Mercury.NetworkError`."
@spec network_error?(error :: term()) :: boolean()
def network_error?(%Mercury.NetworkError{}), do: true
def network_error?(_error), do: false
@doc "Returns `true` if `error` is a `Mercury.TimeoutError`."
@spec timeout_error?(error :: term()) :: boolean()
def timeout_error?(%Mercury.TimeoutError{}), do: true
def timeout_error?(_error), do: false
@doc false
@spec classify(
status :: pos_integer(),
message :: String.t(),
request_id :: String.t() | nil,
resource :: String.t(),
body :: map() | nil
) ::
Exception.t()
def classify(status, message, request_id, resource, body) do
base = %{message: message, status: status, request_id: request_id, body: body}
case status do
401 ->
struct(Mercury.AuthenticationError, Map.put(base, :code, :unauthorized))
403 ->
struct(Mercury.APIError, Map.put(base, :code, :forbidden))
404 ->
struct(Mercury.NotFoundError, Map.merge(base, %{code: :not_found, resource: resource}))
400 ->
struct(Mercury.ValidationError, Map.put(base, :code, :bad_request))
409 ->
struct(Mercury.ConflictError, Map.put(base, :code, :conflict))
status when status >= 500 ->
struct(Mercury.ServerError, Map.put(base, :code, :server_error))
_other ->
struct(Mercury.APIError, Map.put(base, :code, :unknown))
end
end
end