From daf79f5072ed9b9dd16247027a661b894bf9aa55 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Mon, 20 Apr 2026 14:48:22 -0400 Subject: [PATCH 1/2] fix: do not JSON parse arbitrary bodies --- src/workos/_base_client.py | 2 +- tests/test_organizations.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/workos/_base_client.py b/src/workos/_base_client.py index 4d3f07e7..c823b06f 100644 --- a/src/workos/_base_client.py +++ b/src/workos/_base_client.py @@ -189,7 +189,7 @@ def _build_headers( def _deserialize_response( self, response: httpx.Response, model: Optional[Type[Deserializable]] ) -> Any: - if response.status_code == 204 or not response.content: + if response.status_code == 204 or not response.content or model is None: return None data: Dict[str, Any] = cast(Dict[str, Any], response.json()) if model is not None: diff --git a/tests/test_organizations.py b/tests/test_organizations.py index d1a43e5d..18f5d261 100644 --- a/tests/test_organizations.py +++ b/tests/test_organizations.py @@ -108,7 +108,7 @@ def test_update_organization(self, workos, httpx_mock): assert request.url.path.endswith("/organizations/test_id") def test_delete_organization(self, workos, httpx_mock): - httpx_mock.add_response(status_code=204) + httpx_mock.add_response(status_code=202, content=b"\n") result = workos.organizations.delete_organization("test_id") assert result is None request = httpx_mock.get_request() @@ -289,7 +289,7 @@ async def test_update_organization(self, async_workos, httpx_mock): @pytest.mark.asyncio async def test_delete_organization(self, async_workos, httpx_mock): - httpx_mock.add_response(status_code=204) + httpx_mock.add_response(status_code=202, content=b"\n") result = await async_workos.organizations.delete_organization("test_id") assert result is None request = httpx_mock.get_request() From 56fa8a02cd802d3f344a17728355645f3a16b3e9 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Mon, 20 Apr 2026 15:05:33 -0400 Subject: [PATCH 2/2] don't lazily shortcut --- src/workos/_base_client.py | 9 ++++-- tests/test_generated_client.py | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/workos/_base_client.py b/src/workos/_base_client.py index c823b06f..b0120afb 100644 --- a/src/workos/_base_client.py +++ b/src/workos/_base_client.py @@ -189,11 +189,14 @@ def _build_headers( def _deserialize_response( self, response: httpx.Response, model: Optional[Type[Deserializable]] ) -> Any: - if response.status_code == 204 or not response.content or model is None: + if response.status_code == 204 or not response.content: + return None + try: + data = response.json() + except Exception: return None - data: Dict[str, Any] = cast(Dict[str, Any], response.json()) if model is not None: - return model.from_dict(data) + return model.from_dict(cast(Dict[str, Any], data)) return data @staticmethod diff --git a/tests/test_generated_client.py b/tests/test_generated_client.py index b641e750..a675cc43 100644 --- a/tests/test_generated_client.py +++ b/tests/test_generated_client.py @@ -451,6 +451,33 @@ def test_documented_import_surface_exposes_resources(self): assert client.widgets is not None client.close() + def test_request_raw_preserves_json_dict_response(self, httpx_mock): + httpx_mock.add_response(json={"ok": True}) + client = WorkOSClient( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + result = client.request_raw("GET", "test") + assert result == {"ok": True} + client.close() + + def test_request_list_preserves_json_array_response(self, httpx_mock): + httpx_mock.add_response(json=[{"id": "item_123"}]) + client = WorkOSClient( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + result = client.request_list("GET", "test") + assert result == [{"id": "item_123"}] + client.close() + + def test_request_returns_none_for_non_json_success_without_model(self, httpx_mock): + httpx_mock.add_response(status_code=202, content=b"\n") + client = WorkOSClient( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + result = client.request("DELETE", "test") + assert result is None + client.close() + @pytest.mark.asyncio class TestAsyncWorkOSClient: @@ -475,6 +502,35 @@ async def test_documented_import_surface_exposes_resources(self): assert client.widgets is not None await client.close() + async def test_request_raw_preserves_json_dict_response(self, httpx_mock): + httpx_mock.add_response(json={"ok": True}) + client = AsyncWorkOSClient( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + result = await client.request_raw("GET", "test") + assert result == {"ok": True} + await client.close() + + async def test_request_list_preserves_json_array_response(self, httpx_mock): + httpx_mock.add_response(json=[{"id": "item_123"}]) + client = AsyncWorkOSClient( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + result = await client.request_list("GET", "test") + assert result == [{"id": "item_123"}] + await client.close() + + async def test_request_returns_none_for_non_json_success_without_model( + self, httpx_mock + ): + httpx_mock.add_response(status_code=202, content=b"\n") + client = AsyncWorkOSClient( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + result = await client.request("DELETE", "test") + assert result is None + await client.close() + async def test_raises_400(self, httpx_mock): httpx_mock.add_response( status_code=400,