diff --git a/src/functions/src/supabase_functions/_async/functions_client.py b/src/functions/src/supabase_functions/_async/functions_client.py index 2f7529b1..b7a7edc2 100644 --- a/src/functions/src/supabase_functions/_async/functions_client.py +++ b/src/functions/src/supabase_functions/_async/functions_client.py @@ -95,6 +95,13 @@ async def _request( method, str(url), json=json, headers=headers, params=params ) ) + # Relay failures set this header even on non-2xx responses. + if response.headers.get("x-relay-error") == "true": + raise FunctionsRelayError( + response.json().get("error") + or "Relay Error invoking the Edge Function", + response.status_code, + ) try: response.raise_for_status() except HTTPError as exc: @@ -165,10 +172,6 @@ async def invoke( 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")) if response_type == "json": data = response.json() diff --git a/src/functions/src/supabase_functions/_sync/functions_client.py b/src/functions/src/supabase_functions/_sync/functions_client.py index 18f073d7..400814df 100644 --- a/src/functions/src/supabase_functions/_sync/functions_client.py +++ b/src/functions/src/supabase_functions/_sync/functions_client.py @@ -95,6 +95,13 @@ def _request( method, str(url), json=json, headers=headers, params=params ) ) + # Relay failures set this header even on non-2xx responses. + if response.headers.get("x-relay-error") == "true": + raise FunctionsRelayError( + response.json().get("error") + or "Relay Error invoking the Edge Function", + response.status_code, + ) try: response.raise_for_status() except HTTPError as exc: @@ -165,10 +172,6 @@ def invoke( response = 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")) if response_type == "json": data = response.json() diff --git a/src/functions/tests/_async/test_function_client.py b/src/functions/tests/_async/test_function_client.py index a821deb8..f9d728b9 100644 --- a/src/functions/tests/_async/test_function_client.py +++ b/src/functions/tests/_async/test_function_client.py @@ -155,8 +155,9 @@ async def test_invoke_with_http_error(client: AsyncFunctionsClient) -> None: async def test_invoke_with_relay_error(client: AsyncFunctionsClient) -> None: mock_response = Mock(spec=Response) mock_response.json.return_value = {"error": "Relay error message"} + mock_response.status_code = 200 mock_response.raise_for_status = Mock() - mock_response.headers = {"x-relay-header": "true"} + mock_response.headers = {"x-relay-error": "true"} with patch.object( client._client, "request", new_callable=AsyncMock @@ -167,6 +168,29 @@ async def test_invoke_with_relay_error(client: AsyncFunctionsClient) -> None: await client.invoke("test-function") +async def test_invoke_relay_error_on_non_2xx_status( + client: AsyncFunctionsClient, +) -> None: + mock_response = Mock(spec=Response) + mock_response.json.return_value = {"error": "Relay error message"} + mock_response.status_code = 546 + mock_response.raise_for_status.side_effect = HTTPError("HTTP Error") + mock_response.headers = {"x-relay-error": "true"} + + with patch.object( + client._client, "request", new_callable=AsyncMock + ) as mock_request: + mock_request.return_value = mock_response + + with pytest.raises( + FunctionsRelayError, match="Relay error message" + ) as exc_info: + await client.invoke("test-function") + + assert not isinstance(exc_info.value, FunctionsHttpError) + assert exc_info.value.status == 546 + + async def test_invoke_invalid_function_name(client: AsyncFunctionsClient) -> None: with pytest.raises(ValueError, match="function_name must a valid string value."): await client.invoke("") diff --git a/src/functions/tests/_sync/test_function_client.py b/src/functions/tests/_sync/test_function_client.py index 6be348df..e2cb1d83 100644 --- a/src/functions/tests/_sync/test_function_client.py +++ b/src/functions/tests/_sync/test_function_client.py @@ -145,8 +145,9 @@ def test_invoke_with_http_error(client: SyncFunctionsClient) -> None: def test_invoke_with_relay_error(client: SyncFunctionsClient) -> None: mock_response = Mock(spec=Response) mock_response.json.return_value = {"error": "Relay error message"} + mock_response.status_code = 200 mock_response.raise_for_status = Mock() - mock_response.headers = {"x-relay-header": "true"} + mock_response.headers = {"x-relay-error": "true"} with patch.object(client._client, "request", new_callable=Mock) as mock_request: mock_request.return_value = mock_response @@ -155,6 +156,25 @@ def test_invoke_with_relay_error(client: SyncFunctionsClient) -> None: client.invoke("test-function") +def test_invoke_relay_error_on_non_2xx_status(client: SyncFunctionsClient) -> None: + mock_response = Mock(spec=Response) + mock_response.json.return_value = {"error": "Relay error message"} + mock_response.status_code = 546 + mock_response.raise_for_status.side_effect = HTTPError("HTTP Error") + mock_response.headers = {"x-relay-error": "true"} + + with patch.object(client._client, "request", new_callable=Mock) as mock_request: + mock_request.return_value = mock_response + + with pytest.raises( + FunctionsRelayError, match="Relay error message" + ) as exc_info: + client.invoke("test-function") + + assert not isinstance(exc_info.value, FunctionsHttpError) + assert exc_info.value.status == 546 + + def test_invoke_invalid_function_name(client: SyncFunctionsClient) -> None: with pytest.raises(ValueError, match="function_name must a valid string value."): client.invoke("")