Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Changelog

## [Unreleased]

### Added
- Dynamic auth token updates feature for better token rotation support
- `Supabase.Functions.update_auth/2` function for functional client updates
- `:auth` option in `Supabase.Functions.invoke/3` for per-request auth token overrides
- Feature parity with JavaScript client's `setAuth(token)` method
- Comprehensive test coverage for both update methods
- Enhanced documentation with examples for both auth update approaches
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,77 @@ end
Supabase.Functions.invoke(client, "stream-data", on_response: on_response)
# :ok
```

## Timeout Support

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.

```elixir
client = Supabase.init_client!("SUPABASE_URL", "SUPABASE_KEY")

# Basic invocation with default timeout (15 seconds)
{:ok, response} = Supabase.Functions.invoke(client, "my-function")

# Custom timeout (5 seconds)
{:ok, response} = Supabase.Functions.invoke(client, "my-function", timeout: 5_000)

# Timeout with body and headers
{:ok, response} = Supabase.Functions.invoke(client, "my-function",
body: %{data: "value"},
headers: %{"x-custom" => "header"},
timeout: 30_000)

# Streaming with timeout
on_response = fn {status, headers, body} ->
# Handle streaming response
{:ok, body}
end

{:ok, response} = Supabase.Functions.invoke(client, "my-function",
on_response: on_response,
timeout: 10_000)
```

This feature provides:
- **Request cancellation**: Long-running requests will timeout and be cancelled
- **Better resource management**: Prevents hanging connections
- **Comprehensive timeout coverage**: Sets both receive timeout (per-chunk) and request timeout (complete response)
- **Feature parity with JS client**: Matches timeout functionality in the JavaScript SDK

## Dynamic Auth Token Updates

You can update authorization tokens dynamically using two approaches, providing feature parity with the JavaScript client's `setAuth(token)` method:

### Option 1: Functional Update

Create a new client with an updated auth token:

```elixir
client = Supabase.init_client!("SUPABASE_URL", "SUPABASE_KEY")

# Update auth token functionally
updated_client = Supabase.Functions.update_auth(client, "new_jwt_token")
{:ok, response} = Supabase.Functions.invoke(updated_client, "my-function")
```

### Option 2: Per-Request Override

Override the authorization token for a specific request:

```elixir
client = Supabase.init_client!("SUPABASE_URL", "SUPABASE_KEY")

# Use a different token for this request only
{:ok, response} = Supabase.Functions.invoke(client, "my-function", auth: "user_jwt_token")

# Original client remains unchanged for subsequent calls
{:ok, response2} = Supabase.Functions.invoke(client, "another-function")
```

### Benefits

- **Token rotation support**: Easily update tokens without recreating clients
- **Better performance**: Avoid the overhead of creating new client instances
- **Flexibility**: Use different tokens per request or update client globally
- **Feature parity**: Matches the JavaScript client's `setAuth()` functionality
- **Immutability**: Original client instances remain unchanged with functional updates
82 changes: 76 additions & 6 deletions lib/supabase/functions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@ defmodule Supabase.Functions do
- `method`: The HTTP method of the request.
- `region`: The Region to invoke the function in.
- `on_response`: The custom response handler for response streaming.
- `timeout`: The timeout in milliseconds for the request. Defaults to 15 seconds.
- `auth`: Override the authorization token for this request.
"""
@type opt ::
{:body, Fetcher.body()}
| {:headers, Fetcher.headers()}
| {:method, Fetcher.method()}
| {:region, region}
| {:on_response, on_response}
| {:timeout, pos_integer()}
| {:auth, String.t()}

@type on_response :: ({Fetcher.status(), Fetcher.headers(), body :: Enumerable.t()} ->
Supabase.result(Response.t()))
Expand All @@ -52,20 +56,85 @@ defmodule Supabase.Functions do
| :"ca-central-1"
| :"eu-central-1"

@doc """
Updates the access token for a client

Creates a new client instance with the updated access token. This provides
feature parity with the JavaScript client's `setAuth(token)` method.

## Examples

# Update auth token functionally
new_client = Supabase.Functions.update_auth(client, "new_token")
{:ok, response} = Supabase.Functions.invoke(new_client, "my-function")

"""
@spec update_auth(Client.t(), String.t()) :: Client.t()
def update_auth(%Client{} = client, token) when is_binary(token) do
%{client | access_token: token}
end

@doc """
Invokes a function

Invoke a Supabase Edge Function.

- 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.
- Responses are automatically parsed as json depending on the Content-Type header sent by your function. Responses are parsed as text by default.

## Authentication

You can override the authorization token for a specific request using the `auth` option:

# Use a different token for this request
{:ok, response} = Supabase.Functions.invoke(client, "my-function", auth: "new_token")

Alternatively, you can update the client's auth token functionally:

# Update the client's token
new_client = Supabase.Functions.update_auth(client, "new_token")
{:ok, response} = Supabase.Functions.invoke(new_client, "my-function")

## Timeout Support

You can set a timeout for function invocations using the `timeout` option. This sets both the
receive timeout (for individual chunks) and request timeout (for the complete response):

# Timeout after 5 seconds
Supabase.Functions.invoke(client, "my-function", timeout: 5_000)

If no timeout is specified, requests will timeout after 15 seconds by default.

## Examples

# Basic invocation
{:ok, response} = Supabase.Functions.invoke(client, "my-function")

# With timeout
{:ok, response} = Supabase.Functions.invoke(client, "my-function", timeout: 10_000)

# With body and timeout
{:ok, response} = Supabase.Functions.invoke(client, "my-function",
body: %{data: "value"},
timeout: 30_000)

# With custom auth token
{:ok, response} = Supabase.Functions.invoke(client, "my-function", auth: "custom_token")
"""
@spec invoke(Client.t(), function :: String.t(), opts) :: Supabase.result(Response.t())
def invoke(%Client{} = client, name, opts \\ []) when is_binary(name) do
method = opts[:method] || :post
custom_headers = opts[:headers] || %{}
timeout = opts[:timeout] || 15_000

client
# Handle auth token override
effective_client =
case opts[:auth] do
nil -> client
auth_token when is_binary(auth_token) -> update_auth(client, auth_token)
end

effective_client
|> Request.new(decode_body?: false)
|> Request.with_functions_url(name)
|> Request.with_method(method)
Expand All @@ -75,7 +144,7 @@ defmodule Supabase.Functions do
|> Request.with_body_decoder(nil)
|> maybe_define_content_type(opts[:body])
|> Request.with_headers(custom_headers)
|> execute_request(opts[:on_response])
|> execute_request(opts[:on_response], timeout)
|> maybe_decode_body()
|> handle_response()
end
Expand Down Expand Up @@ -103,12 +172,13 @@ defmodule Supabase.Functions do

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

defp execute_request(req, on_response) do
defp execute_request(req, on_response, timeout) do
opts = [receive_timeout: timeout, request_timeout: timeout]

if on_response do
Fetcher.stream(req, on_response)
Fetcher.stream(req, on_response, opts)
else
# consume all the response, answers eagerly
Fetcher.stream(req)
Fetcher.stream(req, nil, opts)
end
end

Expand Down
Loading