From 4984636e7e8aa0340443106c893ffd42ceb4bc24 Mon Sep 17 00:00:00 2001 From: Konrad Rokicki Date: Thu, 27 Aug 2026 22:11:54 -0400 Subject: [PATCH] fix: honour encoding-type=url in file listings get_list_xml's url_encode flag shadowed the url_encode() function it calls to apply EncodingType, so any listing that asked for encoding-type=url raised "TypeError: 'bool' object is not callable" and returned a 500. Only the file client was affected: the aioboto client passes url_encode=False and never reaches the call. Neuroglancer always sends encoding-type=url, so no file target could be browsed from it. Renamed the flag to encode_values, and added a listing test that asks for encoding-type=url on both a plain and a virtual-prefixed target. Co-Authored-By: Claude Opus 5 (1M context) --- pyproject.toml | 2 +- tests/test_file.py | 16 ++++++++++++++++ x2s3/client_aioboto.py | 2 +- x2s3/utils.py | 7 +++++-- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e09e02e..a9a5321 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "x2s3" -version = "1.5.0" +version = "1.5.1" description = "RESTful web service which makes any storage system X available as an S3-compatible REST API" readme = "README.md" license = { file = "LICENSE" } diff --git a/tests/test_file.py b/tests/test_file.py index 0ab04d2..6f338c3 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -368,3 +368,19 @@ def test_virtual_prefix_continuation(app): token = root.find('NextContinuationToken').text assert token.startswith(VIRTUAL_PREFIX) assert paged == all_keys + + +def test_list_objects_encoding_type_url(app): + # Clients such as Neuroglancer always ask for encoding-type=url, which used + # to raise a TypeError because the flag shadowed the encoding function + with TestClient(app) as client: + for bucket, prefix in [('local-files', 'tests/'), + ('mounted-files', f'{VIRTUAL_PREFIX}tests/')]: + response = client.get( + f"/{bucket}?list-type=2&prefix={prefix}&delimiter=/&encoding-type=url") + assert response.status_code == 200 + root = parse_xml(response.text) + assert root.find('Prefix').text == prefix + assert root.find('EncodingType').text == 'url' + assert f"{prefix}test_file.py" in [c.find('Key').text for c in root.findall('Contents')] + assert f"{prefix}java/" in [cp.find('Prefix').text for cp in root.findall('CommonPrefixes')] diff --git a/x2s3/client_aioboto.py b/x2s3/client_aioboto.py index 116a9b8..0d533c2 100644 --- a/x2s3/client_aioboto.py +++ b/x2s3/client_aioboto.py @@ -344,7 +344,7 @@ async def list_objects_v2(self, 'StartAfter': start_after } - xml = get_list_xml(contents, common_prefixes, url_encode=False, **kwargs) + xml = get_list_xml(contents, common_prefixes, encode_values=False, **kwargs) return Response(content=xml, media_type="application/xml") except Exception as e: diff --git a/x2s3/utils.py b/x2s3/utils.py index c29a83b..8e0960d 100644 --- a/x2s3/utils.py +++ b/x2s3/utils.py @@ -101,12 +101,15 @@ def get_bucket_list_xml(buckets): return elem_to_str(root) -def get_list_xml(contents, common_prefixes, url_encode=True, **kwargs): +def get_list_xml(contents, common_prefixes, encode_values=True, **kwargs): """ Creates S3-style XML elements for the given object listing. + + `encode_values` must not be named `url_encode`: that shadowed the + url_encode() function this uses to honour EncodingType=url. """ is_url_encode = False - if url_encode and 'EncodingType' in kwargs: + if encode_values and 'EncodingType' in kwargs: is_url_encode = kwargs['EncodingType']=='url' root = ET.Element("ListBucketResult", xmlns=S3_XMLNS)