defmodule Stripe.Error do @moduledoc """ A struct which represents an error which occurred during a Stripe API call. This struct is designed to provide all the information needed to effectively log and maybe respond to an error. It contains the following fields: - `:source` – this is one of * `:internal` – the error occurred within the library. This is usually caused by an unexpected or missing parameter. * `:network` – the error occurred while making the network request (i.e. `:hackney.request/5` returned an error.) In this case, `:code` will always be `:network_error`. The `:hackney_reason` field in the `:extra` map contains the actual error reason received from hackney. * `:stripe` – an error response was received from Stripe. - `:code` – an atom indicating the particular error. See "Error Codes" for more detail. - `:request_id` – if `:source` is `:stripe`, this will contain the [request ID](https://stripe.com/docs/api#request_ids) for logging and troubleshooting. Otherwise, this field is `nil`. - `:message` – a loggable message describing the error. This should not be shown to your users but is intended for logging and troubleshooting. - `:user_message` – if Stripe has provided a user-facing message (e.g. when a card is declined), this field will contain it. Otherwise it is `nil`. - `:extra` - a map which may contain some additional information about the error. See "Extra Fields" for details. ## Extra Fields The `:extra` field contains a map of miscellaneous information about the error which may be useful. The fields are not present if not relevant. The possible fields are: - `:card_code` – when `:code` is `:card_error`, contains one of Stripe's [decline reasons](https://stripe.com/docs/api#errors). - `:decline_code` – an optional short string provided by the bank when a card is declined. - `:param` – for errors where a particular parameter was the cause, indicates which parameter was invalid. - `:charge_id` – when a Charge was declined, indicates the ID of the failed Charge which was created. - `:http_status` – for `:stripe` errors, the HTTP status returned with the error. - `:raw_error` – the raw error map received from Stripe. - `:hackney_reason` – for `:network` errors, contains the error reason received from hackney. ## Error Codes The `:code` fields may be one of the following: - `:api_connection_error`, `:api_error`, `:authentication_error`, `:card_error`, `:invalid_request_error`, `:rate_limit_error`, `:validation_error` – as per the [Stripe docs](https://stripe.com/docs/api#errors) - `:bad_request`, `:unauthorized`, `:request_failed`, `:not_found`, `:conflict`, `:too_many_requests`, `:server_error`, `:unknown_error` – these only occur if Stripe did not send an explicit `type` for the error. They have the meaning as defined in [Stripe's HTTP status code summary](https://stripe.com/docs/api#errors) - `:network_code` – used only when `:source` is `:network`. Indicates an error occurred while making the request. - `:valid_keys_failed`, `:required_keys_failed`, `:endpoint_fun_invalid_result`, `:invalid_endpoint` – used when `:source` is `:internal`. See `Stripe.Request` for details. """ @type error_source :: :internal | :network | :stripe @type error_status :: :bad_request | :unauthorized | :request_failed | :not_found | :conflict | :too_many_requests | :server_error | :unknown_error @type stripe_error_type :: :api_connection_error | :api_error | :authentication_error | :card_error | :invalid_request_error | :rate_limit_error | :validation_error @type card_error_code :: :invalid_number | :invalid_expiry_month | :invalid_expiry_year | :invalid_cvc | :invalid_swipe_data | :incorrect_number | :expired_card | :incorrect_cvc | :incorrect_zip | :card_declined | :missing | :processing_error | :bank_account_verification_failed | :idempotency_key_in_use @type t :: %__MODULE__{ source: error_source, code: error_status | stripe_error_type | Stripe.Request.error_code() | :network_error, request_id: String.t() | nil, message: String.t(), user_message: String.t() | nil, extra: %{ optional(:card_code) => card_error_code, optional(:decline_code) => String.t(), optional(:param) => String.t(), optional(:charge_id) => Stripe.id(), optional(:http_status) => 400..599, optional(:raw_error) => map, optional(:hackney_reason) => any } } @enforce_keys [:source, :code, :message] defstruct [:source, :code, :request_id, :extra, :message, :user_message] @doc false @spec new(Keyword.t()) :: t def new(fields) do struct!(__MODULE__, fields) end @doc false @spec from_hackney_error(any) :: t def from_hackney_error(reason) do %__MODULE__{ source: :network, code: :network_error, message: "An error occurred while making the network request. The HTTP client returned the following reason: #{inspect(reason)}", extra: %{ hackney_reason: reason } } end @doc false @spec from_stripe_error(400..599, nil, String.t() | nil) :: t def from_stripe_error(status, nil, request_id) do %__MODULE__{ source: :stripe, code: code_from_status(status), request_id: request_id, extra: %{http_status: status}, message: message_from_status(status) } end @spec from_stripe_error(400..599, map, String.t()) :: t def from_stripe_error(status, error_data, request_id) do case error_data |> Map.get("type") |> maybe_to_atom() do nil -> from_stripe_error(status, nil, request_id) type -> stripe_message = Map.get(error_data, "message") user_message = case type do :card_error -> stripe_message _ -> nil end message = stripe_message || message_from_type(type) extra = %{raw_error: error_data, http_status: status} |> maybe_put(:card_code, error_data |> Map.get("code") |> maybe_to_atom()) |> maybe_put(:decline_code, Map.get(error_data, "decline_code")) |> maybe_put(:param, Map.get(error_data, "param")) |> maybe_put(:charge_id, Map.get(error_data, "charge")) %__MODULE__{ source: :stripe, code: type, request_id: request_id, message: message, user_message: user_message, extra: extra } end end defp code_from_status(400), do: :bad_request defp code_from_status(401), do: :unauthorized defp code_from_status(402), do: :request_failed defp code_from_status(404), do: :not_found defp code_from_status(409), do: :conflict defp code_from_status(429), do: :too_many_requests defp code_from_status(s) when s in [500, 502, 503, 504], do: :server_error defp code_from_status(_), do: :unknown_error defp message_from_status(400), do: "The request was unacceptable, often due to missing a required parameter." defp message_from_status(401), do: "No valid API key provided." defp message_from_status(402), do: "The parameters were valid but the request failed." defp message_from_status(404), do: "The requested resource doesn't exist." defp message_from_status(409), do: "The request conflicts with another request (perhaps due to using the same idempotent key)." defp message_from_status(429), do: "Too many requests hit the API too quickly. We recommend an exponential backoff of your requests." defp message_from_status(s) when s in [500, 502, 503, 504], do: "Something went wrong on Stripe's end." defp message_from_status(s), do: "An unknown HTTP code of #{s} was received." defp message_from_type(:api_connection_error), do: "The connection to Stripe's API failed." defp message_from_type(:api_error), do: "An internal Stripe error occurred. This is usually temporary." defp message_from_type(:authentication_error), do: "You failed to properly authenticate yourself in the request." defp message_from_type(:card_error), do: "The card could not be charged for some reason." defp message_from_type(:invalid_request_error), do: "Your request had invalid parameters." defp message_from_type(:rate_limit_error), do: "Too many requests hit the API too quickly. We recommend an exponential backoff of your requests." defp message_from_type(:validation_error), do: "A client-side library failed to validate a field." defp maybe_put(map, _key, nil), do: map defp maybe_put(map, key, value), do: Map.put(map, key, value) defp maybe_to_atom(nil), do: nil # credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom defp maybe_to_atom(string) when is_binary(string), do: String.to_atom(string) end