From 55d119ddbbf723ace6f3b4436ca91346ce9309fd Mon Sep 17 00:00:00 2001 From: dheerajitbhu <73860752+dheerajitbhu@users.noreply.github.com> Date: Mon, 9 Nov 2026 11:08:31 -0800 Subject: [PATCH] fix: return non-JSON-document `application/json*` records as text in `get_record()` --- .../_resource_clients/key_value_store.py | 10 +++-- tests/unit/test_key_value_store.py | 37 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/apify_client/_resource_clients/key_value_store.py b/src/apify_client/_resource_clients/key_value_store.py index 23f7e521..3970b068 100644 --- a/src/apify_client/_resource_clients/key_value_store.py +++ b/src/apify_client/_resource_clients/key_value_store.py @@ -50,10 +50,14 @@ def _parse_get_record_response(response: HttpResponse) -> Any: content_type = response.headers['content-type'].split(';')[0].strip() try: - if re.search(r'^application/json', content_type, flags=re.IGNORECASE): + if re.search(r'^application/json(?:$|.*\+json$)', content_type, flags=re.IGNORECASE): response_data = response.json() - elif re.search(r'^application/.*xml$', content_type, flags=re.IGNORECASE) or re.search( - r'^text/', content_type, flags=re.IGNORECASE + elif ( + # Other `application/json*` types, such as `application/jsonl` or `application/json-seq`, are not + # a single JSON document, so they are returned as text rather than failing to parse. + re.search(r'^application/json', content_type, flags=re.IGNORECASE) + or re.search(r'^application/.*xml$', content_type, flags=re.IGNORECASE) + or re.search(r'^text/', content_type, flags=re.IGNORECASE) ): response_data = response.text else: diff --git a/tests/unit/test_key_value_store.py b/tests/unit/test_key_value_store.py index 3abfe9bb..d4d7c709 100644 --- a/tests/unit/test_key_value_store.py +++ b/tests/unit/test_key_value_store.py @@ -524,3 +524,40 @@ async def test_set_record_streams_pre_encoded_file_like_value_async( assert captured_records[0].headers['content-encoding'] == 'gzip' assert captured_records[0].get_data() == compressed + + +_GET_RECORD_CASES = [ + pytest.param('application/json', b'{"a": 1}', {'a': 1}, id='json is parsed'), + pytest.param('application/json; charset=utf-8', b'[1, 2]', [1, 2], id='json with parameters is parsed'), + pytest.param('application/json-patch+json', b'[{"op": "remove"}]', [{'op': 'remove'}], id='+json is parsed'), + pytest.param('application/jsonl', b'{"a":1}\n{"a":2}', '{"a":1}\n{"a":2}', id='jsonl is returned as text'), + pytest.param('application/json-seq', b'\x1e{"a":1}\n', '\x1e{"a":1}\n', id='json-seq is returned as text'), +] + + +@pytest.mark.parametrize(('content_type', 'body', 'expected_value'), _GET_RECORD_CASES) +def test_get_record_decodes_value_by_content_type_sync( + *, httpserver: HTTPServer, api_url: str, content_type: str, body: bytes, expected_value: Any +) -> None: + """`application/json*` records that are not a single JSON document come back as text instead of failing.""" + httpserver.expect_request(_RECORD_PATH, method='GET').respond_with_data(body, content_type=content_type) + client = ApifyClient(token='test_token', api_url=api_url) + + record = client.key_value_store(_MOCKED_KVS_ID).get_record('f') + + assert record is not None + assert record['value'] == expected_value + + +@pytest.mark.parametrize(('content_type', 'body', 'expected_value'), _GET_RECORD_CASES) +async def test_get_record_decodes_value_by_content_type_async( + *, httpserver: HTTPServer, api_url: str, content_type: str, body: bytes, expected_value: Any +) -> None: + """`application/json*` records that are not a single JSON document come back as text instead of failing.""" + httpserver.expect_request(_RECORD_PATH, method='GET').respond_with_data(body, content_type=content_type) + client = ApifyClientAsync(token='test_token', api_url=api_url) + + record = await client.key_value_store(_MOCKED_KVS_ID).get_record('f') + + assert record is not None + assert record['value'] == expected_value