Skip to content
Open
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
31 changes: 20 additions & 11 deletions src/functions/src/supabase_functions/_async/functions_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
from httpx import AsyncClient, HTTPError, QueryParams, Response
from yarl import URL

from ..errors import FunctionsHttpError, FunctionsRelayError
from ..errors import FunctionsHttpError, FunctionsRelayError, _error_message_from
from ..utils import (
FunctionRegion,
is_http_url,
is_valid_str_arg,
)
from ..version import __version__

HTTPMethod = Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"]


class AsyncFunctionsClient:
def __init__(
Expand Down Expand Up @@ -77,20 +79,19 @@ def __init__(

async def _request(
self,
method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
method: HTTPMethod,
path: list[str],
headers: Optional[Dict[str, str]] = None,
json: Optional[Dict[Any, Any]] = None,
json: Optional[Union[Dict[Any, Any], str, bytes]] = None,
params: Optional[QueryParams] = None,
) -> Response:
url = self.url.joinpath(*path)
headers = headers or dict()
headers.update(self.headers)
headers = {**self.headers, **(headers or {})}
response = (
await self._client.request(
method, str(url), data=json, headers=headers, params=params
method, str(url), content=json, headers=headers, params=params
)
if isinstance(json, str)
if isinstance(json, (str, bytes))
else await self._client.request(
method, str(url), json=json, headers=headers, params=params
)
Expand All @@ -103,7 +104,7 @@ async def _request(
status_code = response.status_code

raise FunctionsHttpError(
response.json().get("error")
_error_message_from(response)
or f"An error occurred while requesting your edge function at {exc.request.url!r}.",
status_code,
) from exc
Expand Down Expand Up @@ -132,17 +133,20 @@ async def invoke(
invoke_options : object with the following properties
`headers`: object representing the headers to send with the request
`body`: the body of the request
`method`: the HTTP method to invoke the function with. The default is `POST`
`responseType`: how the response should be parsed. The default is `json`
"""
if not is_valid_str_arg(function_name):
raise ValueError("function_name must a valid string value.")
headers = self.headers
headers: Dict[str, str] = {}
params = QueryParams()
body = None
method: HTTPMethod = "POST"
response_type = "text/plain"

if invoke_options is not None:
headers.update(invoke_options.get("headers", {}))
method = invoke_options.get("method", "POST")
response_type = invoke_options.get("responseType", "text/plain")

region = invoke_options.get("region")
Expand All @@ -161,14 +165,19 @@ async def invoke(
headers["Content-Type"] = "text/plain"
elif isinstance(body, dict):
headers["Content-Type"] = "application/json"
elif isinstance(body, bytes):
headers["Content-Type"] = "application/octet-stream"

response = await self._request(
"POST", [function_name], headers=headers, json=body, params=params
method, [function_name], headers=headers, json=body, params=params
)
is_relay_error = response.headers.get("x-relay-header")

if is_relay_error and is_relay_error == "true":
raise FunctionsRelayError(response.json().get("error"))
raise FunctionsRelayError(
_error_message_from(response)
or "An error occurred while relaying your edge function request."
)

if response_type == "json":
data = response.json()
Expand Down
31 changes: 20 additions & 11 deletions src/functions/src/supabase_functions/_sync/functions_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
from httpx import Client, HTTPError, QueryParams, Response
from yarl import URL

from ..errors import FunctionsHttpError, FunctionsRelayError
from ..errors import FunctionsHttpError, FunctionsRelayError, _error_message_from
from ..utils import (
FunctionRegion,
is_http_url,
is_valid_str_arg,
)
from ..version import __version__

HTTPMethod = Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"]


class SyncFunctionsClient:
def __init__(
Expand Down Expand Up @@ -77,20 +79,19 @@ def __init__(

def _request(
self,
method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
method: HTTPMethod,
path: list[str],
headers: Optional[Dict[str, str]] = None,
json: Optional[Dict[Any, Any]] = None,
json: Optional[Union[Dict[Any, Any], str, bytes]] = None,
params: Optional[QueryParams] = None,
) -> Response:
url = self.url.joinpath(*path)
headers = headers or dict()
headers.update(self.headers)
headers = {**self.headers, **(headers or {})}
response = (
self._client.request(
method, str(url), data=json, headers=headers, params=params
method, str(url), content=json, headers=headers, params=params
)
if isinstance(json, str)
if isinstance(json, (str, bytes))
else self._client.request(
method, str(url), json=json, headers=headers, params=params
)
Expand All @@ -103,7 +104,7 @@ def _request(
status_code = response.status_code

raise FunctionsHttpError(
response.json().get("error")
_error_message_from(response)
or f"An error occurred while requesting your edge function at {exc.request.url!r}.",
status_code,
) from exc
Expand Down Expand Up @@ -132,17 +133,20 @@ def invoke(
invoke_options : object with the following properties
`headers`: object representing the headers to send with the request
`body`: the body of the request
`method`: the HTTP method to invoke the function with. The default is `POST`
`responseType`: how the response should be parsed. The default is `json`
"""
if not is_valid_str_arg(function_name):
raise ValueError("function_name must a valid string value.")
headers = self.headers
headers: Dict[str, str] = {}
params = QueryParams()
body = None
method: HTTPMethod = "POST"
response_type = "text/plain"

if invoke_options is not None:
headers.update(invoke_options.get("headers", {}))
method = invoke_options.get("method", "POST")
response_type = invoke_options.get("responseType", "text/plain")

region = invoke_options.get("region")
Expand All @@ -161,14 +165,19 @@ def invoke(
headers["Content-Type"] = "text/plain"
elif isinstance(body, dict):
headers["Content-Type"] = "application/json"
elif isinstance(body, bytes):
headers["Content-Type"] = "application/octet-stream"

response = self._request(
"POST", [function_name], headers=headers, json=body, params=params
method, [function_name], headers=headers, json=body, params=params
)
is_relay_error = response.headers.get("x-relay-header")

if is_relay_error and is_relay_error == "true":
raise FunctionsRelayError(response.json().get("error"))
raise FunctionsRelayError(
_error_message_from(response)
or "An error occurred while relaying your edge function request."
)

if response_type == "json":
data = response.json()
Expand Down
20 changes: 19 additions & 1 deletion src/functions/src/supabase_functions/errors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from __future__ import annotations

from typing import TypedDict
from typing import TYPE_CHECKING, TypedDict

if TYPE_CHECKING:
from httpx import Response


class FunctionsApiErrorDict(TypedDict):
Expand Down Expand Up @@ -42,3 +45,18 @@ def __init__(self, message: str, code: int | None = None) -> None:
"FunctionsRelayError",
400 if code is None else code,
)


def _error_message_from(response: Response) -> str | None:
"""Best-effort extraction of an error message from an edge function response.

An edge function may reply with a plain-text or empty body, so a failed JSON
decode must not mask the underlying HTTP error.
"""
try:
body = response.json()
except ValueError:
return response.text or None
if isinstance(body, dict):
return body.get("error")
return response.text or None
Loading
Loading