Skip to content

Commit 9d8338b

Browse files
authored
add timeout support for function invocations (#9)
1 parent 33f9152 commit 9d8338b

File tree

3 files changed

+144
-13
lines changed

3 files changed

+144
-13
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,39 @@ end
8888
Supabase.Functions.invoke(client, "stream-data", on_response: on_response)
8989
# :ok
9090
```
91+
92+
## Timeout Support
93+
94+
You can control the timeout for function invocations using the `timeout` option. If no timeout is specified, requests will timeout after 15 seconds by default.
95+
96+
```elixir
97+
client = Supabase.init_client!("SUPABASE_URL", "SUPABASE_KEY")
98+
99+
# Basic invocation with default timeout (15 seconds)
100+
{:ok, response} = Supabase.Functions.invoke(client, "my-function")
101+
102+
# Custom timeout (5 seconds)
103+
{:ok, response} = Supabase.Functions.invoke(client, "my-function", timeout: 5_000)
104+
105+
# Timeout with body and headers
106+
{:ok, response} = Supabase.Functions.invoke(client, "my-function",
107+
body: %{data: "value"},
108+
headers: %{"x-custom" => "header"},
109+
timeout: 30_000)
110+
111+
# Streaming with timeout
112+
on_response = fn {status, headers, body} ->
113+
# Handle streaming response
114+
{:ok, body}
115+
end
116+
117+
{:ok, response} = Supabase.Functions.invoke(client, "my-function",
118+
on_response: on_response,
119+
timeout: 10_000)
120+
```
121+
122+
This feature provides:
123+
- **Request cancellation**: Long-running requests will timeout and be cancelled
124+
- **Better resource management**: Prevents hanging connections
125+
- **Comprehensive timeout coverage**: Sets both receive timeout (per-chunk) and request timeout (complete response)
126+
- **Feature parity with JS client**: Matches timeout functionality in the JavaScript SDK

lib/supabase/functions.ex

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ defmodule Supabase.Functions do
2424
- `method`: The HTTP method of the request.
2525
- `region`: The Region to invoke the function in.
2626
- `on_response`: The custom response handler for response streaming.
27+
- `timeout`: The timeout in milliseconds for the request. Defaults to 15 seconds.
2728
"""
2829
@type opt ::
2930
{:body, Fetcher.body()}
3031
| {:headers, Fetcher.headers()}
3132
| {:method, Fetcher.method()}
3233
| {:region, region}
3334
| {:on_response, on_response}
35+
| {:timeout, pos_integer()}
3436

3537
@type on_response :: ({Fetcher.status(), Fetcher.headers(), body :: Enumerable.t()} ->
3638
Supabase.result(Response.t()))
@@ -59,11 +61,35 @@ defmodule Supabase.Functions do
5961
6062
- When you pass in a body to your function, we automatically attach the `Content-Type` header automatically. If it doesn't match any of these types we assume the payload is json, serialize it and attach the `Content-Type` header as `application/json`. You can override this behavior by passing in a `Content-Type` header of your own.
6163
- Responses are automatically parsed as json depending on the Content-Type header sent by your function. Responses are parsed as text by default.
64+
65+
## Timeout Support
66+
67+
You can set a timeout for function invocations using the `timeout` option. This sets both the
68+
receive timeout (for individual chunks) and request timeout (for the complete response):
69+
70+
# Timeout after 5 seconds
71+
Supabase.Functions.invoke(client, "my-function", timeout: 5_000)
72+
73+
If no timeout is specified, requests will timeout after 15 seconds by default.
74+
75+
## Examples
76+
77+
# Basic invocation
78+
{:ok, response} = Supabase.Functions.invoke(client, "my-function")
79+
80+
# With timeout
81+
{:ok, response} = Supabase.Functions.invoke(client, "my-function", timeout: 10_000)
82+
83+
# With body and timeout
84+
{:ok, response} = Supabase.Functions.invoke(client, "my-function",
85+
body: %{data: "value"},
86+
timeout: 30_000)
6287
"""
6388
@spec invoke(Client.t(), function :: String.t(), opts) :: Supabase.result(Response.t())
6489
def invoke(%Client{} = client, name, opts \\ []) when is_binary(name) do
6590
method = opts[:method] || :post
6691
custom_headers = opts[:headers] || %{}
92+
timeout = opts[:timeout] || 15_000
6793

6894
client
6995
|> Request.new(decode_body?: false)
@@ -75,7 +101,7 @@ defmodule Supabase.Functions do
75101
|> Request.with_body_decoder(nil)
76102
|> maybe_define_content_type(opts[:body])
77103
|> Request.with_headers(custom_headers)
78-
|> execute_request(opts[:on_response])
104+
|> execute_request(opts[:on_response], timeout)
79105
|> maybe_decode_body()
80106
|> handle_response()
81107
end
@@ -103,12 +129,13 @@ defmodule Supabase.Functions do
103129

104130
defp raw_binary?(bin), do: not String.printable?(bin)
105131

106-
defp execute_request(req, on_response) do
132+
defp execute_request(req, on_response, timeout) do
133+
opts = [receive_timeout: timeout, request_timeout: timeout]
134+
107135
if on_response do
108-
Fetcher.stream(req, on_response)
136+
Fetcher.stream(req, on_response, opts)
109137
else
110-
# consume all the response, answers eagerly
111-
Fetcher.stream(req)
138+
Fetcher.stream(req, nil, opts)
112139
end
113140
end
114141

test/supabase/functions_test.exs

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ defmodule Supabase.FunctionsTest do
3434
end
3535

3636
test "handles text response content type", %{client: client} do
37-
expect(@mock, :stream, fn _request, _ ->
37+
expect(@mock, :stream, fn _request, _opts ->
3838
{:ok,
3939
%Finch.Response{
4040
status: 200,
@@ -50,7 +50,7 @@ defmodule Supabase.FunctionsTest do
5050
test "sets appropriate content-type for binary data", %{client: client} do
5151
binary_data = <<0, 1, 2, 3>>
5252

53-
expect(@mock, :stream, fn request, _ ->
53+
expect(@mock, :stream, fn request, _opts ->
5454
assert Request.get_header(request, "content-type") == "application/octet-stream"
5555
assert request.body == binary_data
5656

@@ -71,7 +71,7 @@ defmodule Supabase.FunctionsTest do
7171
test "sets appropriate content-type for JSON data", %{client: client} do
7272
json_data = %{test: "data"}
7373

74-
expect(@mock, :stream, fn request, _ ->
74+
expect(@mock, :stream, fn request, _opts ->
7575
assert Request.get_header(request, "content-type") == "application/json"
7676
# fetcher will io encode it
7777
assert {:ok, _} = Jason.decode(request.body)
@@ -93,7 +93,7 @@ defmodule Supabase.FunctionsTest do
9393
test "handles custom headers", %{client: client} do
9494
custom_headers = %{"x-custom-header" => "test-value"}
9595

96-
expect(@mock, :stream, fn request, _ ->
96+
expect(@mock, :stream, fn request, _opts ->
9797
assert Request.get_header(request, "x-custom-header") == "test-value"
9898

9999
{:ok,
@@ -116,7 +116,7 @@ defmodule Supabase.FunctionsTest do
116116
test "handles streaming responses with custom handler", %{client: client} do
117117
chunks = ["chunk1", "chunk2", "chunk3"]
118118

119-
expect(@mock, :stream, fn _request, on_response, _ ->
119+
expect(@mock, :stream, fn _request, on_response, _opts ->
120120
Enum.each(chunks, fn chunk ->
121121
on_response.({200, %{"content-type" => "text/plain"}, [chunk]})
122122
end)
@@ -141,7 +141,7 @@ defmodule Supabase.FunctionsTest do
141141
end
142142

143143
test "handles error responses", %{client: client} do
144-
expect(@mock, :stream, fn _request, _ ->
144+
expect(@mock, :stream, fn _request, _opts ->
145145
{:ok,
146146
%Finch.Response{
147147
status: 404,
@@ -156,7 +156,7 @@ defmodule Supabase.FunctionsTest do
156156
end
157157

158158
test "uses custom HTTP method when specified", %{client: client} do
159-
expect(@mock, :stream, fn request, _ ->
159+
expect(@mock, :stream, fn request, _opts ->
160160
assert request.method == :get
161161

162162
{:ok,
@@ -174,7 +174,7 @@ defmodule Supabase.FunctionsTest do
174174
end
175175

176176
test "handles relay errors", %{client: client} do
177-
expect(@mock, :stream, fn _request, _ ->
177+
expect(@mock, :stream, fn _request, _opts ->
178178
{:ok,
179179
%Finch.Response{
180180
status: 200,
@@ -188,5 +188,73 @@ defmodule Supabase.FunctionsTest do
188188

189189
assert error.code == :relay_error
190190
end
191+
192+
test "passes timeout option to underlying HTTP client", %{client: client} do
193+
expect(@mock, :stream, fn _request, opts ->
194+
assert Keyword.get(opts, :receive_timeout) == 5_000
195+
assert Keyword.get(opts, :request_timeout) == 5_000
196+
197+
{:ok,
198+
%Finch.Response{
199+
status: 200,
200+
headers: %{"content-type" => "application/json"},
201+
body: ~s({"success": true})
202+
}}
203+
end)
204+
205+
assert {:ok, response} =
206+
Functions.invoke(client, "test-function", timeout: 5_000, http_client: @mock)
207+
208+
assert response.body == %{"success" => true}
209+
end
210+
211+
test "uses default timeout when not specified", %{client: client} do
212+
expect(@mock, :stream, fn _request, opts ->
213+
assert Keyword.get(opts, :receive_timeout) == 15_000
214+
assert Keyword.get(opts, :request_timeout) == 15_000
215+
216+
{:ok,
217+
%Finch.Response{
218+
status: 200,
219+
headers: %{"content-type" => "application/json"},
220+
body: ~s({"success": true})
221+
}}
222+
end)
223+
224+
assert {:ok, response} =
225+
Functions.invoke(client, "test-function", http_client: @mock)
226+
227+
assert response.body == %{"success" => true}
228+
end
229+
230+
test "timeout works with streaming response", %{client: client} do
231+
chunks = ["chunk1", "chunk2"]
232+
233+
expect(@mock, :stream, fn _request, on_response, opts ->
234+
assert Keyword.get(opts, :receive_timeout) == 2_000
235+
assert Keyword.get(opts, :request_timeout) == 2_000
236+
237+
Enum.each(chunks, fn chunk ->
238+
on_response.({200, %{"content-type" => "text/plain"}, [chunk]})
239+
end)
240+
241+
{:ok, Enum.join(chunks)}
242+
end)
243+
244+
on_response = fn {status, headers, body} ->
245+
assert status == 200
246+
assert headers["content-type"] == "text/plain"
247+
{:ok, body}
248+
end
249+
250+
assert {:ok, response} =
251+
Functions.invoke(client, "test-function",
252+
on_response: on_response,
253+
timeout: 2_000,
254+
http_client: @mock
255+
)
256+
257+
assert response == "chunk1chunk2"
258+
end
191259
end
192260
end

0 commit comments

Comments
 (0)