From e9a27b584e13e019b9178e076c157c98b3db44b0 Mon Sep 17 00:00:00 2001 From: Kamil Monicz Date: Sat, 7 Feb 2026 06:01:19 +0000 Subject: [PATCH] Fix empty payloads --- src/connectrpc/_client_shared.py | 11 +++++---- src/connectrpc/_server_async.py | 2 +- src/connectrpc/_server_sync.py | 2 +- test/test_roundtrip.py | 40 ++++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/connectrpc/_client_shared.py b/src/connectrpc/_client_shared.py index 0b91777..00ec65a 100644 --- a/src/connectrpc/_client_shared.py +++ b/src/connectrpc/_client_shared.py @@ -30,11 +30,12 @@ def prepare_get_params( codec: Codec, request_data: bytes, headers: HTTPHeaders ) -> dict[str, str]: - params = {"connect": f"v{CONNECT_PROTOCOL_VERSION}"} - if request_data: - params["message"] = base64.urlsafe_b64encode(request_data).decode("ascii") - params["base64"] = "1" - params["encoding"] = codec.name() + params = { + "connect": f"v{CONNECT_PROTOCOL_VERSION}", + "message": base64.urlsafe_b64encode(request_data).decode("ascii"), + "base64": "1", + "encoding": codec.name(), + } if "content-encoding" in headers: params["compression"] = headers.pop("content-encoding") return params diff --git a/src/connectrpc/_server_async.py b/src/connectrpc/_server_async.py index cadb456..72ad2c2 100644 --- a/src/connectrpc/_server_async.py +++ b/src/connectrpc/_server_async.py @@ -194,7 +194,7 @@ async def __call__( if http_method == "GET": query_string = scope.get("query_string", b"").decode("utf-8") - query_params = parse_qs(query_string) + query_params = parse_qs(query_string, keep_blank_values=True) codec_name = query_params.get("encoding", ("",))[0] else: query_params = _UNSET_QUERY_PARAMS diff --git a/src/connectrpc/_server_sync.py b/src/connectrpc/_server_sync.py index 027485f..e340acd 100644 --- a/src/connectrpc/_server_sync.py +++ b/src/connectrpc/_server_sync.py @@ -353,7 +353,7 @@ def _handle_get_request( """Handle GET request with query parameters.""" try: query_string = environ.get("QUERY_STRING", "") - params = parse_qs(query_string) + params = parse_qs(query_string, keep_blank_values=True) if "message" not in params: raise ConnectError( diff --git a/test/test_roundtrip.py b/test/test_roundtrip.py index 21386c5..56a4f13 100644 --- a/test/test_roundtrip.py +++ b/test/test_roundtrip.py @@ -70,6 +70,46 @@ async def make_hat(self, request, ctx): assert response.color == "green" +def test_roundtrip_sync_connect_get_empty_request() -> None: + class RoundtripHaberdasherSync(HaberdasherSync): + def make_hat(self, request, ctx): + return Hat(size=request.inches, color="green") + + compression = resolve_compression("identity") + app = HaberdasherWSGIApplication( + RoundtripHaberdasherSync(), compressions=[compression] + ) + with HaberdasherClientSync( + "http://localhost", + http_client=SyncClient(WSGITransport(app=app)), + send_compression=compression, + accept_compression=[compression], + ) as client: + response = client.make_hat(request=Size(), use_get=True) + assert response.size == 0 + assert response.color == "green" + + +@pytest.mark.asyncio +async def test_roundtrip_async_connect_get_empty_request() -> None: + class RoundtripHaberdasher(Haberdasher): + async def make_hat(self, request, ctx): + return Hat(size=request.inches, color="green") + + compression = resolve_compression("identity") + app = HaberdasherASGIApplication(RoundtripHaberdasher(), compressions=[compression]) + transport = ASGITransport(app) + async with HaberdasherClient( + "http://localhost", + http_client=Client(transport=transport), + send_compression=compression, + accept_compression=[compression], + ) as client: + response = await client.make_hat(request=Size(), use_get=True) + assert response.size == 0 + assert response.color == "green" + + @pytest.mark.parametrize("proto_json", [False, True]) @pytest.mark.parametrize("compression_name", ["gzip", "br", "zstd", "identity"]) @pytest.mark.asyncio