diff --git a/moto/s3/responses.py b/moto/s3/responses.py index b0a9ddbea2d..64fc2e1812d 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -3,6 +3,8 @@ import re from typing import List, Union +import urllib.parse + from moto import settings from moto.core.utils import ( amzn_request_id, @@ -633,6 +635,12 @@ def _handle_list_objects_v2(self, bucket_name, querystring): key_count = len(result_keys) + len(result_folders) + if encoding_type == "url": + prefix = urllib.parse.quote(prefix) if prefix else "" + result_folders = list( + map(lambda folder: urllib.parse.quote(folder), result_folders) + ) + return template.render( bucket=bucket, prefix=prefix or "", diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 6ba2e8533e2..4d3d63916a9 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -3364,3 +3364,26 @@ def test_head_versioned_key_in_not_versioned_bucket(): response = ex.value.response assert response["Error"]["Code"] == "400" + + +@mock_s3 +def test_prefix_encoding(): + bucket_name = "encoding-bucket" + client = boto3.client("s3", region_name=DEFAULT_REGION_NAME) + client.create_bucket(Bucket=bucket_name) + + client.put_object(Bucket=bucket_name, Key="foo%2Fbar/data", Body=b"") + + data = client.list_objects_v2(Bucket=bucket_name, Prefix="foo%2Fbar") + assert data["Contents"][0]["Key"].startswith(data["Prefix"]) + + data = client.list_objects_v2(Bucket=bucket_name, Prefix="foo%2Fbar", Delimiter="/") + assert data["CommonPrefixes"] == [{"Prefix": "foo%2Fbar/"}] + + client.put_object(Bucket=bucket_name, Key="foo/bar/data", Body=b"") + + data = client.list_objects_v2(Bucket=bucket_name, Delimiter="/") + folders = list( + map(lambda common_prefix: common_prefix["Prefix"], data["CommonPrefixes"]) + ) + assert ["foo%2Fbar/", "foo/"] == folders