Skip to content

invoke() never raises FunctionsRelayError for real relay failures #1637

Description

@hsusul

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.

  1. The client reads x-relay-header. The relay, supabase-js, supabase-swift, and supabase_flutter all use x-relay-error.
  2. _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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingfunctionsparitypythonPull requests that update Python code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions