Current section

Files

Jump to
supabase_potion lib supabase error.ex
Raw

lib/supabase/error.ex

defmodule Supabase.Error do
@moduledoc """
Represents and centralizes error responses within the Supabase ecosystem.
The `Supabase.Error` struct is a unified way to handle error responses,
providing fields to represent key attributes of an error while remaining flexible
enough to accommodate custom implementations.
## Fields
- `code` (atom): A semantic representation of the error code, e.g., `:not_found` or `:unauthorized`.
- `message` (String.t): A human-readable message describing the error.
- `service` (Supabase.service()): The service from which the error originated (e.g., `:auth`, `:storage`).
- `metadata` (map): Additional information to provide context about the error,
such as the request path, headers, or response body.
## Example
%Supabase.Error{
code: :not_found,
message: "Resource Not Found",
service: :storage,
metadata: %{
path: "/api/resource",
req_body: %{},
resp_body: "Not found",
headers: [{"content-type", "application/json"}]
}
}
## Custom Error Handling
Libraries or users may define custom error parsers by implementing the
`Supabase.Error` behaviour's `from/2` callback from the `Supabase.ErrorParser`
protocol.
This enables the transformation of ANY structure into meaningful errors
specific to their application domain.
"""
alias Supabase.Fetcher.Request
alias Supabase.Fetcher.Response
@type t :: %__MODULE__{
code: atom,
message: String.t(),
service: Supabase.service() | nil,
metadata: map
}
defstruct [:message, :service, code: :unexpected, metadata: %{}]
@doc "Callback used on invoking the HTTP error parsed on a response (status >= 400)"
@callback from(source :: term, context :: term) :: t
@doc "Creates a new `Supabase.Error` struct based on informed options"
@spec new(keyword) :: t
def new(attrs) when is_list(attrs) do
code = Keyword.get(attrs, :code, :unexpected)
message = Keyword.get(attrs, :message, humanize_error_code(code))
service = Keyword.get(attrs, :service)
metadata = Keyword.get(attrs, :metadata, %{})
%__MODULE__{code: code, message: message, service: service, metadata: metadata}
end
@doc "Helper to just transform an atom code into a more human-friendly string"
@spec humanize_error_code(atom) :: String.t()
def humanize_error_code(code) when is_atom(code) do
code
|> Atom.to_string()
|> String.split("_", trim: true)
|> Enum.map_join(" ", &String.capitalize/1)
end
@doc """
Helper function to construct the metadata fields for when building
an error from a HTTP response, based into the "context", aka `Supabase.Request`.
"""
@spec make_default_http_metadata(Supabase.Fetcher.Request.t()) :: map
def make_default_http_metadata(%Supabase.Fetcher.Request{} = ctx) do
service_key = if service = ctx.service, do: :"#{service}_url"
base_url = Map.get(ctx.client, service_key)
path = String.replace(to_string(ctx.url), base_url, "")
headers = Enum.reject(ctx.headers, &(String.downcase(elem(&1, 0)) == "authorization"))
%{
path: path,
req_body: ctx.body,
headers: headers
}
end
defimpl Inspect, for: Supabase.Error do
import Inspect.Algebra
def inspect(
%Supabase.Error{code: code, message: message, service: service, metadata: metadata},
opts
) do
concat([
"#Supabase.Error<",
to_doc([code: code, message: message, service: service], opts),
if(metadata != %{}, do: concat([", metadata: ", to_doc(metadata, opts)]), else: ""),
">"
])
end
end
end
defmodule Supabase.HTTPErrorParser do
@moduledoc """
The default error parser in case no one is provided via `Supabase.Fetcher.with_error_parser/2`.
Error parsers should be implement firstly by adjacent services libraries, to
handle service-specific error like for authentication or storage, although
a final user could easily attach their own custom error parser.
The default error parser define the `code` and `message` fields based into
the HTTP Status.
The default `metadata` format is:
%{
path: "The URL path appended to the base_url in request",
req_body: "The request body, encoded as iodata or binary",
resp_body: "The response body as it is",
# headers is a list of tuples (String.t, String.t)
# the `authorization` header is removed from it
headers: []
}
All other fields are filled with the `Supabase.Request` struct as context,
if available.
"""
alias Supabase.Fetcher.Request
alias Supabase.Fetcher.Response
@behaviour Supabase.Error
def from(%Response{} = resp, %Request{service: service} = ctx)
when resp.status >= 400 do
code = parse_status(resp.status)
metadata = Supabase.Error.make_default_http_metadata(ctx)
metadata = Map.merge(metadata, %{resp_status: resp.status, resp_body: resp.body})
Supabase.Error.new(code: code, service: service, metadata: metadata)
end
defp parse_status(400), do: :bad_request
defp parse_status(401), do: :unauthorized
defp parse_status(403), do: :forbidden
defp parse_status(404), do: :not_found
defp parse_status(405), do: :method_not_allowed
defp parse_status(409), do: :resource_already_exists
defp parse_status(411), do: :missing_content_length
defp parse_status(413), do: :content_too_large
defp parse_status(416), do: :invalid_range
defp parse_status(422), do: :unprocessable_entity
defp parse_status(423), do: :resource_locked
defp parse_status(429), do: :too_many_requests
defp parse_status(500), do: :server_error
defp parse_status(501), do: :not_implemented
defp parse_status(503), do: :service_unavailable
defp parse_status(504), do: :gateway_timeout
defp parse_status(_), do: :unexpected
end