From 5578ea257b90ff3ecf3a88b4ef979761a08fcbd4 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 30 Sep 2025 16:51:53 -0300 Subject: [PATCH 1/3] feat: add region as forceFunctionRegion query parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated FunctionsClient (both async and sync) to add region as both x-region header and forceFunctionRegion query param - Used urllib.parse.urlencode to properly construct query parameters - Added comprehensive tests to verify both header and query parameter functionality - Updated existing tests to check for both region mechanisms - Maintains backward compatibility with existing x-region header Ported from https://github.com/supabase/functions-js/pull/100 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/supabase_functions/_async/functions_client.py | 7 ++++++- .../src/supabase_functions/_sync/functions_client.py | 7 ++++++- src/functions/tests/_async/test_function_client.py | 10 ++++++++-- src/functions/tests/_sync/test_function_client.py | 10 ++++++++-- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/functions/src/supabase_functions/_async/functions_client.py b/src/functions/src/supabase_functions/_async/functions_client.py index 38ed3c61..b5bf33ab 100644 --- a/src/functions/src/supabase_functions/_async/functions_client.py +++ b/src/functions/src/supabase_functions/_async/functions_client.py @@ -1,4 +1,5 @@ from typing import Any, Dict, Literal, Optional, Union +from urllib.parse import urlencode from warnings import warn from httpx import AsyncClient, HTTPError, Response @@ -123,6 +124,8 @@ async def invoke( headers = self.headers body = None response_type = "text/plain" + url = f"{self.url}/{function_name}" + if invoke_options is not None: headers.update(invoke_options.get("headers", {})) response_type = invoke_options.get("responseType", "text/plain") @@ -135,6 +138,8 @@ async def invoke( if region.value != "any": headers["x-region"] = region.value + # Add region as query parameter + url = f"{url}?{urlencode({'forceFunctionRegion': region.value})}" body = invoke_options.get("body") if isinstance(body, str): @@ -143,7 +148,7 @@ async def invoke( headers["Content-Type"] = "application/json" response = await self._request( - "POST", f"{self.url}/{function_name}", headers=headers, json=body + "POST", url, headers=headers, json=body ) is_relay_error = response.headers.get("x-relay-header") diff --git a/src/functions/src/supabase_functions/_sync/functions_client.py b/src/functions/src/supabase_functions/_sync/functions_client.py index 95d30e42..77e1c43a 100644 --- a/src/functions/src/supabase_functions/_sync/functions_client.py +++ b/src/functions/src/supabase_functions/_sync/functions_client.py @@ -1,4 +1,5 @@ from typing import Any, Dict, Literal, Optional, Union +from urllib.parse import urlencode from warnings import warn from httpx import Client, HTTPError, Response @@ -123,6 +124,8 @@ def invoke( headers = self.headers body = None response_type = "text/plain" + url = f"{self.url}/{function_name}" + if invoke_options is not None: headers.update(invoke_options.get("headers", {})) response_type = invoke_options.get("responseType", "text/plain") @@ -135,6 +138,8 @@ def invoke( if region.value != "any": headers["x-region"] = region.value + # Add region as query parameter + url = f"{url}?{urlencode({'forceFunctionRegion': region.value})}" body = invoke_options.get("body") if isinstance(body, str): @@ -143,7 +148,7 @@ def invoke( headers["Content-Type"] = "application/json" response = self._request( - "POST", f"{self.url}/{function_name}", headers=headers, json=body + "POST", url, headers=headers, json=body ) is_relay_error = response.headers.get("x-relay-header") diff --git a/src/functions/tests/_async/test_function_client.py b/src/functions/tests/_async/test_function_client.py index f9b7e61d..85d611ea 100644 --- a/src/functions/tests/_async/test_function_client.py +++ b/src/functions/tests/_async/test_function_client.py @@ -100,8 +100,11 @@ async def test_invoke_with_region(client: AsyncFunctionsClient): await client.invoke("test-function", {"region": FunctionRegion("us-east-1")}) - _, kwargs = mock_request.call_args + args, kwargs = mock_request.call_args + # Check that x-region header is present assert kwargs["headers"]["x-region"] == "us-east-1" + # Check that the URL contains the forceFunctionRegion query parameter + assert "forceFunctionRegion=us-east-1" in args[1] async def test_invoke_with_region_string(client: AsyncFunctionsClient): @@ -118,8 +121,11 @@ async def test_invoke_with_region_string(client: AsyncFunctionsClient): with pytest.warns(UserWarning, match=r"Use FunctionRegion\(us-east-1\)"): await client.invoke("test-function", {"region": "us-east-1"}) - _, kwargs = mock_request.call_args + args, kwargs = mock_request.call_args + # Check that x-region header is present assert kwargs["headers"]["x-region"] == "us-east-1" + # Check that the URL contains the forceFunctionRegion query parameter + assert "forceFunctionRegion=us-east-1" in args[1] async def test_invoke_with_http_error(client: AsyncFunctionsClient): diff --git a/src/functions/tests/_sync/test_function_client.py b/src/functions/tests/_sync/test_function_client.py index 7f2b8194..8cfff93d 100644 --- a/src/functions/tests/_sync/test_function_client.py +++ b/src/functions/tests/_sync/test_function_client.py @@ -94,8 +94,11 @@ def test_invoke_with_region(client: SyncFunctionsClient): client.invoke("test-function", {"region": FunctionRegion("us-east-1")}) - _, kwargs = mock_request.call_args + args, kwargs = mock_request.call_args + # Check that x-region header is present assert kwargs["headers"]["x-region"] == "us-east-1" + # Check that the URL contains the forceFunctionRegion query parameter + assert "forceFunctionRegion=us-east-1" in args[1] def test_invoke_with_region_string(client: SyncFunctionsClient): @@ -110,8 +113,11 @@ def test_invoke_with_region_string(client: SyncFunctionsClient): with pytest.warns(UserWarning, match=r"Use FunctionRegion\(us-east-1\)"): client.invoke("test-function", {"region": "us-east-1"}) - _, kwargs = mock_request.call_args + args, kwargs = mock_request.call_args + # Check that x-region header is present assert kwargs["headers"]["x-region"] == "us-east-1" + # Check that the URL contains the forceFunctionRegion query parameter + assert "forceFunctionRegion=us-east-1" in args[1] def test_invoke_with_http_error(client: SyncFunctionsClient): From 175f8a00ef5b643d013c9d714bcaaacf3b22326c Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 30 Sep 2025 16:53:40 -0300 Subject: [PATCH 2/3] style: code formatted --- .../src/supabase_functions/_async/functions_client.py | 4 +--- .../src/supabase_functions/_sync/functions_client.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/functions/src/supabase_functions/_async/functions_client.py b/src/functions/src/supabase_functions/_async/functions_client.py index b5bf33ab..a470bc65 100644 --- a/src/functions/src/supabase_functions/_async/functions_client.py +++ b/src/functions/src/supabase_functions/_async/functions_client.py @@ -147,9 +147,7 @@ async def invoke( elif isinstance(body, dict): headers["Content-Type"] = "application/json" - response = await self._request( - "POST", url, headers=headers, json=body - ) + response = await self._request("POST", url, headers=headers, json=body) is_relay_error = response.headers.get("x-relay-header") if is_relay_error and is_relay_error == "true": diff --git a/src/functions/src/supabase_functions/_sync/functions_client.py b/src/functions/src/supabase_functions/_sync/functions_client.py index 77e1c43a..a473d290 100644 --- a/src/functions/src/supabase_functions/_sync/functions_client.py +++ b/src/functions/src/supabase_functions/_sync/functions_client.py @@ -147,9 +147,7 @@ def invoke( elif isinstance(body, dict): headers["Content-Type"] = "application/json" - response = self._request( - "POST", url, headers=headers, json=body - ) + response = self._request("POST", url, headers=headers, json=body) is_relay_error = response.headers.get("x-relay-header") if is_relay_error and is_relay_error == "true": From d729f878d15590dc859d709e0147b8ee3de7a24e Mon Sep 17 00:00:00 2001 From: Leonardo Santiago Date: Wed, 1 Oct 2025 10:35:17 -0300 Subject: [PATCH 3/3] fix: pass `params` to `httpx` request instead of encoding it in url --- .../_async/functions_client.py | 19 +++++++++++++------ .../_sync/functions_client.py | 15 +++++++++------ .../tests/_async/test_function_client.py | 4 ++-- .../tests/_sync/test_function_client.py | 4 ++-- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/functions/src/supabase_functions/_async/functions_client.py b/src/functions/src/supabase_functions/_async/functions_client.py index a470bc65..ea2c502c 100644 --- a/src/functions/src/supabase_functions/_async/functions_client.py +++ b/src/functions/src/supabase_functions/_async/functions_client.py @@ -1,8 +1,7 @@ from typing import Any, Dict, Literal, Optional, Union -from urllib.parse import urlencode from warnings import warn -from httpx import AsyncClient, HTTPError, Response +from httpx import AsyncClient, HTTPError, Response, QueryParams from ..errors import FunctionsHttpError, FunctionsRelayError from ..utils import ( @@ -74,11 +73,16 @@ async def _request( url: str, headers: Optional[Dict[str, str]] = None, json: Optional[Dict[Any, Any]] = None, + params: Optional[QueryParams] = None, ) -> Response: response = ( - await self._client.request(method, url, data=json, headers=headers) + await self._client.request( + method, url, data=json, headers=headers, params=params + ) if isinstance(json, str) - else await self._client.request(method, url, json=json, headers=headers) + else await self._client.request( + method, url, json=json, headers=headers, params=params + ) ) try: response.raise_for_status() @@ -122,6 +126,7 @@ async def invoke( 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" url = f"{self.url}/{function_name}" @@ -139,7 +144,7 @@ async def invoke( if region.value != "any": headers["x-region"] = region.value # Add region as query parameter - url = f"{url}?{urlencode({'forceFunctionRegion': region.value})}" + params = params.set("forceFunctionRegion", region.value) body = invoke_options.get("body") if isinstance(body, str): @@ -147,7 +152,9 @@ async def invoke( elif isinstance(body, dict): headers["Content-Type"] = "application/json" - response = await self._request("POST", url, headers=headers, json=body) + response = await self._request( + "POST", url, headers=headers, json=body, params=params + ) is_relay_error = response.headers.get("x-relay-header") if is_relay_error and is_relay_error == "true": diff --git a/src/functions/src/supabase_functions/_sync/functions_client.py b/src/functions/src/supabase_functions/_sync/functions_client.py index a473d290..8063a7d3 100644 --- a/src/functions/src/supabase_functions/_sync/functions_client.py +++ b/src/functions/src/supabase_functions/_sync/functions_client.py @@ -1,8 +1,7 @@ from typing import Any, Dict, Literal, Optional, Union -from urllib.parse import urlencode from warnings import warn -from httpx import Client, HTTPError, Response +from httpx import Client, HTTPError, Response, QueryParams from ..errors import FunctionsHttpError, FunctionsRelayError from ..utils import ( @@ -74,11 +73,14 @@ def _request( url: str, headers: Optional[Dict[str, str]] = None, json: Optional[Dict[Any, Any]] = None, + params: Optional[QueryParams] = None, ) -> Response: response = ( - self._client.request(method, url, data=json, headers=headers) + self._client.request(method, url, data=json, headers=headers, params=params) if isinstance(json, str) - else self._client.request(method, url, json=json, headers=headers) + else self._client.request( + method, url, json=json, headers=headers, params=params + ) ) try: response.raise_for_status() @@ -122,6 +124,7 @@ def invoke( 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" url = f"{self.url}/{function_name}" @@ -139,7 +142,7 @@ def invoke( if region.value != "any": headers["x-region"] = region.value # Add region as query parameter - url = f"{url}?{urlencode({'forceFunctionRegion': region.value})}" + params = params.set("forceFunctionRegion", region.value) body = invoke_options.get("body") if isinstance(body, str): @@ -147,7 +150,7 @@ def invoke( elif isinstance(body, dict): headers["Content-Type"] = "application/json" - response = self._request("POST", url, headers=headers, json=body) + response = self._request("POST", url, headers=headers, json=body, params=params) is_relay_error = response.headers.get("x-relay-header") if is_relay_error and is_relay_error == "true": diff --git a/src/functions/tests/_async/test_function_client.py b/src/functions/tests/_async/test_function_client.py index 85d611ea..d1f47b97 100644 --- a/src/functions/tests/_async/test_function_client.py +++ b/src/functions/tests/_async/test_function_client.py @@ -104,7 +104,7 @@ async def test_invoke_with_region(client: AsyncFunctionsClient): # Check that x-region header is present assert kwargs["headers"]["x-region"] == "us-east-1" # Check that the URL contains the forceFunctionRegion query parameter - assert "forceFunctionRegion=us-east-1" in args[1] + assert kwargs["params"]["forceFunctionRegion"] == "us-east-1" async def test_invoke_with_region_string(client: AsyncFunctionsClient): @@ -125,7 +125,7 @@ async def test_invoke_with_region_string(client: AsyncFunctionsClient): # Check that x-region header is present assert kwargs["headers"]["x-region"] == "us-east-1" # Check that the URL contains the forceFunctionRegion query parameter - assert "forceFunctionRegion=us-east-1" in args[1] + assert kwargs["params"]["forceFunctionRegion"] == "us-east-1" async def test_invoke_with_http_error(client: AsyncFunctionsClient): diff --git a/src/functions/tests/_sync/test_function_client.py b/src/functions/tests/_sync/test_function_client.py index 8cfff93d..6489e91a 100644 --- a/src/functions/tests/_sync/test_function_client.py +++ b/src/functions/tests/_sync/test_function_client.py @@ -98,7 +98,7 @@ def test_invoke_with_region(client: SyncFunctionsClient): # Check that x-region header is present assert kwargs["headers"]["x-region"] == "us-east-1" # Check that the URL contains the forceFunctionRegion query parameter - assert "forceFunctionRegion=us-east-1" in args[1] + assert kwargs["params"]["forceFunctionRegion"] == "us-east-1" def test_invoke_with_region_string(client: SyncFunctionsClient): @@ -117,7 +117,7 @@ def test_invoke_with_region_string(client: SyncFunctionsClient): # Check that x-region header is present assert kwargs["headers"]["x-region"] == "us-east-1" # Check that the URL contains the forceFunctionRegion query parameter - assert "forceFunctionRegion=us-east-1" in args[1] + assert kwargs["params"]["forceFunctionRegion"] == "us-east-1" def test_invoke_with_http_error(client: SyncFunctionsClient):