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 1/5] 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 From 6ed8b638ec44683426af4cf0b462e1654a2ef45e Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 10 Nov 2026 10:07:08 +0100 Subject: [PATCH 2/5] style: reword JSON content-type comment in `get_record()` --- src/apify_client/_resource_clients/key_value_store.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apify_client/_resource_clients/key_value_store.py b/src/apify_client/_resource_clients/key_value_store.py index 3970b068..7d3de8b5 100644 --- a/src/apify_client/_resource_clients/key_value_store.py +++ b/src/apify_client/_resource_clients/key_value_store.py @@ -53,8 +53,8 @@ def _parse_get_record_response(response: HttpResponse) -> Any: if re.search(r'^application/json(?:$|.*\+json$)', content_type, flags=re.IGNORECASE): response_data = response.json() 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. + # Other `application/json*` types, such as `application/jsonl` or `application/json-seq`, are not a + # single JSON document, so they are returned as text. 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) From 801eb9bf7bd4bd96ddeb558b65baf8dfdbbfe61c Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 10 Nov 2026 10:07:10 +0100 Subject: [PATCH 3/5] test: reword `get_record()` content-type test docstrings --- tests/unit/test_key_value_store.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_key_value_store.py b/tests/unit/test_key_value_store.py index d4d7c709..72ffae27 100644 --- a/tests/unit/test_key_value_store.py +++ b/tests/unit/test_key_value_store.py @@ -539,7 +539,7 @@ async def test_set_record_streams_pre_encoded_file_like_value_async( 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.""" + """JSON document records are parsed, and other `application/json*` records are returned as text.""" httpserver.expect_request(_RECORD_PATH, method='GET').respond_with_data(body, content_type=content_type) client = ApifyClient(token='test_token', api_url=api_url) @@ -553,7 +553,7 @@ def test_get_record_decodes_value_by_content_type_sync( 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.""" + """JSON document records are parsed, and other `application/json*` records are returned as text.""" httpserver.expect_request(_RECORD_PATH, method='GET').respond_with_data(body, content_type=content_type) client = ApifyClientAsync(token='test_token', api_url=api_url) From 48e7adde8d11e98948ed8fb48d3e7f7e2ad5a1a7 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 10 Nov 2026 10:12:47 +0100 Subject: [PATCH 4/5] test: cover uppercase and vendor `+json` content types in `get_record()` --- tests/unit/test_key_value_store.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/test_key_value_store.py b/tests/unit/test_key_value_store.py index 72ffae27..f7522be5 100644 --- a/tests/unit/test_key_value_store.py +++ b/tests/unit/test_key_value_store.py @@ -532,6 +532,8 @@ async def test_set_record_streams_pre_encoded_file_like_value_async( 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.param('APPLICATION/JSONL', b'{"a":1}\n{"a":2}', '{"a":1}\n{"a":2}', id='uppercase jsonl is text'), + pytest.param('application/vnd.api+json', b'{"a": 1}', b'{"a": 1}', id='vnd +json is returned as bytes'), ] From e490a882fc31089a85fa3cefdb1b5423781a389e Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 10 Nov 2026 10:12:51 +0100 Subject: [PATCH 5/5] test: drop underscore prefix from `GET_RECORD_CASES` --- tests/unit/test_key_value_store.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_key_value_store.py b/tests/unit/test_key_value_store.py index f7522be5..e9352ca9 100644 --- a/tests/unit/test_key_value_store.py +++ b/tests/unit/test_key_value_store.py @@ -526,7 +526,7 @@ async def test_set_record_streams_pre_encoded_file_like_value_async( assert captured_records[0].get_data() == compressed -_GET_RECORD_CASES = [ +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'), @@ -537,7 +537,7 @@ async def test_set_record_streams_pre_encoded_file_like_value_async( ] -@pytest.mark.parametrize(('content_type', 'body', 'expected_value'), _GET_RECORD_CASES) +@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: @@ -551,7 +551,7 @@ def test_get_record_decodes_value_by_content_type_sync( assert record['value'] == expected_value -@pytest.mark.parametrize(('content_type', 'body', 'expected_value'), _GET_RECORD_CASES) +@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: