Bug report
FunctionsClient.invoke() is supposed to raise FunctionsRelayError when the Supabase relay cannot reach the Edge Function. Two bugs in _request / invoke mean that never happens for a real relay response.
- The client reads
x-relay-header. The relay, supabase-js, supabase-swift, and supabase_flutter all use x-relay-error.
_request calls raise_for_status() before any relay-header check. Relay failures are non-2xx (see supabase-swift#1112), so they become FunctionsHttpError even if the header name were corrected.
|
try: |
|
response.raise_for_status() |
|
except HTTPError as exc: |
|
status_code = None |
|
if hasattr(response, "status_code"): |
|
status_code = response.status_code |
|
|
|
raise FunctionsHttpError( |
|
response.json().get("error") |
|
or f"An error occurred while requesting your edge function at {exc.request.url!r}.", |
|
status_code, |
|
) from exc |
|
|
|
return response |
|
|
|
def set_auth(self, token: str) -> None: |
|
"""Updates the authorization header |
|
|
|
Parameters |
|
---------- |
|
token : str |
|
the new jwt token sent in the authorization header |
|
""" |
|
|
|
self.headers["Authorization"] = f"Bearer {token}" |
|
|
|
async def invoke( |
|
self, function_name: str, invoke_options: Optional[Dict] = None |
|
) -> Union[Dict, bytes]: |
|
"""Invokes a function |
|
|
|
Parameters |
|
---------- |
|
function_name : the name of the function to invoke |
|
invoke_options : object with the following properties |
|
`headers`: object representing the headers to send with the request |
|
`body`: the body of the request |
|
`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 |
|
params = QueryParams() |
|
body = None |
|
response_type = "text/plain" |
|
|
|
if invoke_options is not None: |
|
headers.update(invoke_options.get("headers", {})) |
|
response_type = invoke_options.get("responseType", "text/plain") |
|
|
|
region = invoke_options.get("region") |
|
if region: |
|
if not isinstance(region, FunctionRegion): |
|
warn(f"Use FunctionRegion({region})", stacklevel=2) |
|
region = FunctionRegion(region) |
|
|
|
if region.value != "any": |
|
headers["x-region"] = region.value |
|
# Add region as query parameter |
|
params = params.set("forceFunctionRegion", region.value) |
|
|
|
body = invoke_options.get("body") |
|
if isinstance(body, str): |
|
headers["Content-Type"] = "text/plain" |
|
elif isinstance(body, dict): |
|
headers["Content-Type"] = "application/json" |
|
|
|
response = await self._request( |
|
"POST", [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")) |
# _request
response.raise_for_status() # 5xx relay failure -> FunctionsHttpError
# invoke, after a 2xx response
is_relay_error = response.headers.get("x-relay-header") # never set by the relay
if is_relay_error and is_relay_error == "true":
raise FunctionsRelayError(response.json().get("error"))
supabase-js checks the real header first:
const isRelayError = response.headers.get('x-relay-error')
if (isRelayError && isRelayError === 'true') {
throw new FunctionsRelayError(response)
}
if (!response.ok) {
throw new FunctionsHttpError(response)
}
The existing Python tests mock x-relay-header on a 200, so they pass while production except FunctionsRelayError is dead. The sync client has the same code. Open PRs #1576 and #1600 still use x-relay-header.
Reproduction
from unittest.mock import Mock, patch
from httpx import HTTPError, Response
from supabase_functions import SyncFunctionsClient
from supabase_functions.errors import FunctionsHttpError, FunctionsRelayError
client = SyncFunctionsClient(
"https://example.functions.supabase.co",
{"Authorization": "Bearer token"},
)
relay_response = Mock(spec=Response)
relay_response.json.return_value = {"error": "Relay error message"}
relay_response.status_code = 546
relay_response.raise_for_status.side_effect = HTTPError("HTTP Error")
relay_response.headers = {"x-relay-error": "true"}
with patch.object(client._client, "request", return_value=relay_response):
try:
client.invoke("hello")
except Exception as exc:
print(type(exc).__name__, exc)
Expected behavior
Raises FunctionsRelayError (matching supabase-js / the functions error-handling docs).
Actual behavior
Raises FunctionsHttpError: Relay error message. A 200 with x-relay-error: true returns the body as success.
System information
- supabase-py:
main @ bb7ecc5
- Package:
supabase_functions (async and sync)
- Python 3.13
Bug report
FunctionsClient.invoke()is supposed to raiseFunctionsRelayErrorwhen the Supabase relay cannot reach the Edge Function. Two bugs in_request/invokemean that never happens for a real relay response.x-relay-header. The relay, supabase-js, supabase-swift, and supabase_flutter all usex-relay-error._requestcallsraise_for_status()before any relay-header check. Relay failures are non-2xx (see supabase-swift#1112), so they becomeFunctionsHttpErroreven if the header name were corrected.supabase-py/src/functions/src/supabase_functions/_async/functions_client.py
Lines 98 to 171 in bb7ecc5
supabase-js checks the real header first:
The existing Python tests mock
x-relay-headeron a 200, so they pass while productionexcept FunctionsRelayErroris dead. The sync client has the same code. Open PRs #1576 and #1600 still usex-relay-header.Reproduction
Expected behavior
Raises
FunctionsRelayError(matching supabase-js / the functions error-handling docs).Actual behavior
Raises
FunctionsHttpError: Relay error message. A 200 withx-relay-error: truereturns the body as success.System information
main@ bb7ecc5supabase_functions(async and sync)