Skip to content

Commit e17d8bd

Browse files
committed
feat: simplify error handler
1 parent 18d1dc4 commit e17d8bd

File tree

9 files changed

+115
-120
lines changed

9 files changed

+115
-120
lines changed

.formatter.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Used by "mix format"
22
[
3-
inputs: ["{mix,.formatter}.exs", "{config,apps}/**/*.{ex,exs}"]
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
44
]

lib/supabase/error.ex

+18-38
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ defmodule Supabase.Error do
3737
specific to their application domain.
3838
"""
3939

40+
alias Supabase.Fetcher.Request
41+
alias Supabase.Fetcher.Response
42+
4043
@type t :: %__MODULE__{
4144
code: atom,
4245
message: String.t(),
@@ -46,6 +49,9 @@ defmodule Supabase.Error do
4649

4750
defstruct [:message, :service, code: :unexpected, metadata: %{}]
4851

52+
@doc "Callback used on invoking the HTTP error parsed on a response (status >= 400)"
53+
@callback from(source :: term, context :: term) :: t
54+
4955
@doc "Creates a new `Supabase.Error` struct based on informed options"
5056
@spec new(keyword) :: t
5157
def new(attrs) when is_list(attrs) do
@@ -101,41 +107,8 @@ defmodule Supabase.Error do
101107
end
102108
end
103109

104-
defprotocol Supabase.ErrorParser do
105-
@spec from(source :: term, context :: term | nil) :: Supabase.Error.t()
106-
def from(source, context \\ nil)
107-
end
108-
109-
defimpl Supabase.ErrorParser, for: File.Error do
110-
def from(%File.Error{} = err, %Supabase.Fetcher.Request{} = ctx) do
111-
message = File.Error.message(err)
112-
metadata = Supabase.Error.make_default_http_metadata(ctx)
113-
114-
Supabase.Error.new(
115-
code: :transport_error,
116-
message: message,
117-
service: ctx.service,
118-
metadata: metadata
119-
)
120-
end
121-
122-
def from(%File.Error{} = err, _) do
123-
message = File.Error.message(err)
124-
125-
Supabase.Error.new(
126-
code: err.reason,
127-
message: message
128-
)
129-
end
130-
end
131-
132-
defimpl Supabase.ErrorParser, for: Supabase.Fetcher.Response do
133-
@moduledoc "The default error parser, generally used to return unexpected errors"
134-
135-
alias Supabase.Fetcher.Request
136-
alias Supabase.Fetcher.Response
137-
138-
@doc """
110+
defmodule Supabase.HTTPErrorParser do
111+
@moduledoc """
139112
The default error parser in case no one is provided via `Supabase.Fetcher.with_error_parser/2`.
140113
141114
Error parsers should be implement firstly by adjacent services libraries, to
@@ -156,10 +129,17 @@ defimpl Supabase.ErrorParser, for: Supabase.Fetcher.Response do
156129
headers: []
157130
}
158131
159-
All other fields are filled with the `Supabase.Fetcher` struct as context.
132+
All other fields are filled with the `Supabase.Request` struct as context,
133+
if available.
160134
"""
161-
@impl true
162-
def from(%Response{} = resp, %Request{service: service} = ctx) do
135+
136+
alias Supabase.Fetcher.Request
137+
alias Supabase.Fetcher.Response
138+
139+
@behaviour Supabase.Error
140+
141+
def from(%Response{} = resp, %Request{service: service} = ctx)
142+
when resp.status >= 400 do
163143
code = parse_status(resp.status)
164144
metadata = Supabase.Error.make_default_http_metadata(ctx)
165145
metadata = Map.merge(metadata, %{resp_status: resp.status, resp_body: resp.body})

lib/supabase/fetcher.ex

+45-20
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ defmodule Supabase.Fetcher do
158158
{:ok, ResponseAdapter.from(resp)}
159159
end
160160
|> handle_response(builder)
161+
rescue
162+
exception -> handle_exception(exception, __STACKTRACE__, builder)
161163
end
162164

163165
@doc """
@@ -182,6 +184,8 @@ defmodule Supabase.Fetcher do
182184
{:ok, ResponseAdapter.from(resp)}
183185
end
184186
|> handle_response(builder)
187+
rescue
188+
exception -> handle_exception(exception, __STACKTRACE__, builder)
185189
end
186190

187191
@doc """
@@ -208,6 +212,8 @@ defmodule Supabase.Fetcher do
208212
{:ok, ResponseAdapter.from(resp)}
209213
end
210214
|> handle_response(builder)
215+
rescue
216+
exception -> handle_exception(exception, __STACKTRACE__, builder)
211217
end
212218

213219
def stream(%Request{http_client: http_client} = builder, on_response, opts)
@@ -216,6 +222,8 @@ defmodule Supabase.Fetcher do
216222
{:ok, ResponseAdapter.from(resp)}
217223
end
218224
|> handle_response(builder)
225+
rescue
226+
exception -> handle_exception(exception, __STACKTRACE__, builder)
219227
end
220228

221229
@doc """
@@ -229,38 +237,34 @@ defmodule Supabase.Fetcher do
229237
end
230238
|> handle_response(builder)
231239
rescue
232-
e in File.Error -> {:error, Supabase.ErrorParser.from(e)}
240+
exception -> handle_exception(exception, __STACKTRACE__, builder)
233241
end
234242

235243
@spec handle_response({:ok, response} | {:error, term}, context) :: Supabase.result(response)
236244
when response: Response.t(),
237245
context: Request.t()
238246
defp handle_response({:ok, %Response{} = resp}, %Request{} = builder) do
239-
error_parser = builder.error_parser
247+
decode_body? = builder.options[:decode_body?] || true
248+
parse_http_err? = builder.options[:parse_http_error?] || true
249+
http_error_parser = builder.error_parser
240250
decoder = builder.body_decoder
241251
decoder_opts = builder.body_decoder_opts
242252

243-
with {:ok, resp} <- Response.decode_body(resp, decoder, decoder_opts) do
244-
if resp.status >= 400 do
245-
{:error, error_parser.from(resp, builder)}
253+
maybe_decode_body = fn ->
254+
if decode_body? do
255+
Response.decode_body(resp, decoder, decoder_opts)
256+
else
257+
resp
258+
end
259+
end
260+
261+
with {:ok, resp} <- maybe_decode_body.() do
262+
if parse_http_err? and resp.status >= 400 do
263+
{:error, http_error_parser.from(resp, builder)}
246264
else
247265
{:ok, resp}
248266
end
249267
end
250-
rescue
251-
e in Protocol.UndefinedError ->
252-
reraise e, __STACKTRACE__
253-
254-
exception ->
255-
message = Exception.format(:error, exception)
256-
stacktrace = Exception.format_stacktrace(__STACKTRACE__)
257-
258-
Supabase.Error.new(
259-
code: :decode_body_failed,
260-
message: message,
261-
service: builder.service,
262-
metadata: %{stacktrace: stacktrace}
263-
)
264268
end
265269

266270
defp handle_response({:error, %Error{} = err}, %Request{} = builder) do
@@ -270,7 +274,28 @@ defmodule Supabase.Fetcher do
270274
end
271275

272276
defp handle_response({:error, err}, %Request{} = builder) do
273-
{:error, Supabase.ErrorParser.from(err, builder)}
277+
metadata = Error.make_default_http_metadata(builder)
278+
279+
{:error,
280+
Error.new(
281+
code: :unexpected,
282+
service: builder.service,
283+
metadata: Map.put(metadata, :raw_error, err)
284+
)}
285+
end
286+
287+
defp handle_exception(exception, stacktrace, %Request{} = builder) do
288+
entity = Map.get(exception, :__struct__)
289+
message = entity.message(exception)
290+
stacktrace = Exception.format_stacktrace(stacktrace)
291+
292+
{:error,
293+
Supabase.Error.new(
294+
code: :exception,
295+
message: message,
296+
service: builder.service,
297+
metadata: %{stacktrace: stacktrace, exception: exception}
298+
)}
274299
end
275300

276301
@doc """

lib/supabase/fetcher/adapter/finch.ex

-28
Original file line numberDiff line numberDiff line change
@@ -135,32 +135,4 @@ defmodule Supabase.Fetcher.Adapter.Finch do
135135
%Supabase.Fetcher.Response{status: resp.status, headers: resp.headers, body: resp.body}
136136
end
137137
end
138-
139-
defimpl Supabase.ErrorParser, for: Mint.TransportError do
140-
def from(%Mint.TransportError{} = err, %Supabase.Fetcher.Request{} = ctx) do
141-
message = Mint.TransportError.message(err)
142-
metadata = Supabase.Error.make_default_http_metadata(ctx)
143-
144-
Supabase.Error.new(
145-
code: :transport_error,
146-
message: message,
147-
service: ctx.service,
148-
metadata: metadata
149-
)
150-
end
151-
end
152-
153-
defimpl Supabase.ErrorParser, for: Mint.HTTPError do
154-
def from(%Mint.HTTPError{} = err, %Supabase.Fetcher.Request{} = ctx) do
155-
message = Mint.HTTPError.message(err)
156-
metadata = Supabase.Error.make_default_http_metadata(ctx)
157-
158-
Supabase.Error.new(
159-
code: :http_error,
160-
message: message,
161-
service: ctx.service,
162-
metadata: metadata
163-
)
164-
end
165-
end
166138
end

lib/supabase/fetcher/request.ex

+15-7
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ defmodule Supabase.Fetcher.Request do
6666

6767
@behaviour Supabase.Fetcher.Request.Behaviour
6868

69+
@type options :: list({:decode_body?, boolean} | {:parse_http_error?, boolean})
70+
6971
@type t :: %__MODULE__{
7072
client: Client.t(),
7173
method: Supabase.Fetcher.method(),
@@ -77,7 +79,8 @@ defmodule Supabase.Fetcher.Request do
7779
body_decoder: module,
7880
body_decoder_opts: keyword,
7981
error_parser: module,
80-
http_client: module
82+
http_client: module,
83+
options: options
8184
}
8285

8386
defstruct [
@@ -90,8 +93,9 @@ defmodule Supabase.Fetcher.Request do
9093
headers: [],
9194
body_decoder_opts: [],
9295
body_decoder: Supabase.Fetcher.JSONDecoder,
93-
error_parser: Supabase.ErrorParser,
94-
http_client: Supabase.Fetcher.Adapter.Finch
96+
error_parser: Supabase.HTTPErrorParser,
97+
http_client: Supabase.Fetcher.Adapter.Finch,
98+
options: [decode_body?: false, parse_http_error?: false]
9599
]
96100

97101
@doc """
@@ -100,13 +104,14 @@ defmodule Supabase.Fetcher.Request do
100104
easily composed using the `with_` functions of this module.
101105
"""
102106
@impl true
103-
def new(%Client{global: global} = client) do
107+
def new(%Client{global: global} = client, opts \\ []) when is_list(opts) do
104108
headers =
105109
global.headers
106110
|> Map.put("authorization", "Bearer " <> client.access_token)
107111
|> Map.to_list()
108112

109113
%__MODULE__{client: client, headers: headers}
114+
|> Map.update!(:options, &Keyword.merge(&1, opts))
110115
end
111116

112117
@services [:auth, :functions, :storage, :realtime, :database]
@@ -139,11 +144,14 @@ defmodule Supabase.Fetcher.Request do
139144
end
140145

141146
@doc """
142-
Attaches a custom error parser to be called after a successfull response.
147+
Attaches a custom error parser to be called after a "successfull" response.
143148
The error parser should implement the `Supabase.Error` behaviour, and it default
144-
to the `Supabase.ErrorParser`.
149+
to the `Supabase.HTTPErrorParser`.
150+
151+
"successful" response means that the fetcher actually got a HTTP response from
152+
the server, so if the HTTP status is >= 400, so this error parser will be invoked.
145153
146-
THis attribute can't be overwritten.
154+
Check `Supabase.HTTPErrorParser` for an example of implementation.
147155
"""
148156
@impl true
149157
def with_error_parser(%__MODULE__{} = builder, parser)

lib/supabase/fetcher/request/behaviour.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ defmodule Supabase.Fetcher.Request.Behaviour do
1616
@callback with_body(Request.t(), body) :: Request.t()
1717
when body: Jason.Encoder.t() | {:stream, Enumerable.t()} | nil
1818
@callback with_headers(Request.t(), headers) :: Request.t()
19-
when headers: Supabase.Fetcher.headers()
19+
when headers: Supabase.Fetcher.headers()
2020
@callback with_body_decoder(Request.t(), decoder, decoder_opts) :: Request.t()
2121
when decoder: module | decoder_fun,
2222
decoder_opts: keyword,

lib/supabase/fetcher/response.ex

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ defmodule Supabase.Fetcher.Response do
55
same structure
66
"""
77

8-
@type t :: %__MODULE__{status: Supabase.Fetcher.status(), headers: Supabase.Fetcher.headers(), body: Supabase.Fetcher.body()}
8+
@type t :: %__MODULE__{
9+
status: Supabase.Fetcher.status(),
10+
headers: Supabase.Fetcher.headers(),
11+
body: Supabase.Fetcher.body()
12+
}
913

1014
defstruct [:status, :headers, :body]
1115

lib/supabase/missing_supabase_config.ex

+20-20
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,32 @@ defmodule Supabase.MissingSupabaseConfig do
1010
Missing #{missing} configuration for your Supabase Client #{if client, do: client, else: ""}.
1111
1212
#{if client do
13-
"""
14-
Please ensure or add the following to your config/config.exs file:
13+
"""
14+
Please ensure or add the following to your config/config.exs file:
1515
16-
import Config
16+
import Config
1717
18-
config :supabase_potion, #{client},
19-
base_url: "https://<app-name>.supabase.co",
20-
api_key: "<supabase-api-key>",
21-
conn: %{access_token: "<supabase-access-token>"},
22-
db: %{schema: "another"}, # default to public
23-
auth: %{debug: true}
18+
config :supabase_potion, #{client},
19+
base_url: "https://<app-name>.supabase.co",
20+
api_key: "<supabase-api-key>",
21+
conn: %{access_token: "<supabase-access-token>"},
22+
db: %{schema: "another"}, # default to public
23+
auth: %{debug: true}
2424
25-
Remember to set the environment variables SUPABASE_BASE_URL and SUPABASE_API_KEY
26-
if you choose this option. Otherwise you can pass the values directly to the config file.
27-
"""
25+
Remember to set the environment variables SUPABASE_BASE_URL and SUPABASE_API_KEY
26+
if you choose this option. Otherwise you can pass the values directly to the config file.
27+
"""
2828
end}
2929
3030
#{if is_nil(client) do
31-
"""
32-
Please ensure you're passing the values directly to the `Supabase.init_client/3` function:
33-
34-
iex> Supabase.init_client!(
35-
iex> System.fetch_env!("SUPABASE_BASE_URL"),
36-
iex> System.fetch_env!("SUPABASE_API_KEY"),
37-
iex> )
38-
"""
31+
"""
32+
Please ensure you're passing the values directly to the `Supabase.init_client/3` function:
33+
34+
iex> Supabase.init_client!(
35+
iex> System.fetch_env!("SUPABASE_BASE_URL"),
36+
iex> System.fetch_env!("SUPABASE_API_KEY"),
37+
iex> )
38+
"""
3939
end}
4040
4141
#{walktrough}

0 commit comments

Comments
 (0)