From c646af8988212ff71419b249de536ca62b627e01 Mon Sep 17 00:00:00 2001 From: Benjamin Simon Date: Fri, 11 Jul 2025 12:26:33 +0200 Subject: [PATCH] fix S3 CORS + GZIP handling --- .../localstack/services/s3/cors.py | 12 +++---- tests/aws/services/s3/test_s3_cors.py | 36 ++++++++++++++++++- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/localstack-core/localstack/services/s3/cors.py b/localstack-core/localstack/services/s3/cors.py index 82193557571f0..9051f3679c6ca 100644 --- a/localstack-core/localstack/services/s3/cors.py +++ b/localstack-core/localstack/services/s3/cors.py @@ -97,12 +97,6 @@ def handle_cors(self, chain: HandlerChain, context: RequestContext, response: Re https://docs.aws.amazon.com/AmazonS3/latest/userguide/cors.html https://docs.aws.amazon.com/AmazonS3/latest/userguide/ManageCorsUsing.html """ - - # this is used with the new ASF S3 provider - # although, we could use it to pre-parse the request and set the context to move the service name parser - if config.DISABLE_CUSTOM_CORS_S3: - return - request = context.request is_s3, bucket_name = self.pre_parse_s3_request(context.request) @@ -111,8 +105,14 @@ def handle_cors(self, chain: HandlerChain, context: RequestContext, response: Re return # set the service so that the regular CORS enforcer knows it needs to ignore this request + # we always want to set the service early, because the `ContentDecoder` is very early in the chain and + # depends on S3 context.service = self._service + if config.DISABLE_CUSTOM_CORS_S3: + # we do not apply S3 specific headers if this config flag is set + return + is_options_request = request.method == "OPTIONS" def stop_options_chain(): diff --git a/tests/aws/services/s3/test_s3_cors.py b/tests/aws/services/s3/test_s3_cors.py index 916efe830d552..4e513d5be6804 100644 --- a/tests/aws/services/s3/test_s3_cors.py +++ b/tests/aws/services/s3/test_s3_cors.py @@ -1,3 +1,6 @@ +import gzip +from io import BytesIO + import pytest import requests import xmltodict @@ -14,7 +17,7 @@ from localstack.testing.config import TEST_AWS_ACCESS_KEY_ID from localstack.testing.pytest import markers from localstack.utils.aws.request_context import mock_aws_request_headers -from localstack.utils.strings import short_uid +from localstack.utils.strings import checksum_crc32, short_uid def _bucket_url_vhost(bucket_name: str, region: str = "", localstack_host: str = None) -> str: @@ -791,3 +794,34 @@ def test_delete_cors(self, s3_bucket, snapshot, aws_client): aws_client.s3.get_bucket_cors(Bucket=s3_bucket) snapshot.match("get-cors-deleted", e.value.response) + + @markers.aws.only_localstack + def test_s3_cors_disabled(self, s3_bucket, aws_client, monkeypatch): + monkeypatch.setattr(config, "DISABLE_CUSTOM_CORS_S3", True) + # the ContentDecoder handler depends on the S3 CORS/pre-process handler to determine the service name + + data = "1234567890" + # Write contents to memory rather than a file. + upload_file_object = BytesIO() + mtime = 1676569620 # hardcode the GZIP timestamp + with gzip.GzipFile(fileobj=upload_file_object, mode="w", mtime=mtime) as filestream: + filestream.write(data.encode("utf-8")) + + raw_gzip_value = upload_file_object.getvalue() + checksum = checksum_crc32(raw_gzip_value) + + # Upload gzip + put_obj = aws_client.s3.put_object( + Bucket=s3_bucket, + Key="test.gz", + ContentEncoding="gzip", + Body=raw_gzip_value, + ) + assert put_obj["ChecksumCRC32"] == checksum + + get_obj = aws_client.s3.get_object( + Bucket=s3_bucket, + Key="test.gz", + ) + assert get_obj["ContentEncoding"] == "gzip" + assert get_obj["Body"].read() == raw_gzip_value