From a5405fb82d8c87475f0941b08991df32d90b756a Mon Sep 17 00:00:00 2001 From: ArfiArfath21 Date: Fri, 16 Oct 2026 23:31:01 +0530 Subject: [PATCH] fix(streaming): report S3Object as closed after close() without building the transformed stream S3Object.closed created the transformed stream on demand. After close() on an object that was never read, a gzip transform wrapped the closed raw stream and reported closed=False, and a zip transform issued S3 requests just to answer. --- .../utilities/streaming/s3_object.py | 6 ++++- .../streaming/_boto3/test_s3_object.py | 25 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/utilities/streaming/s3_object.py b/aws_lambda_powertools/utilities/streaming/s3_object.py index fcbb87b51d6..74b09c14cba 100644 --- a/aws_lambda_powertools/utilities/streaming/s3_object.py +++ b/aws_lambda_powertools/utilities/streaming/s3_object.py @@ -204,7 +204,11 @@ def tell(self) -> int: @property def closed(self) -> bool: - return self.transformed_stream.closed + # Don't build the transformed stream just to answer this: it can issue S3 requests (ZipFile reads the + # central directory on creation), and after close() it would wrap an already closed raw stream. + if self._transformed_stream is None: + return self.raw_stream.closed + return self._transformed_stream.closed def __enter__(self): return self diff --git a/tests/functional/streaming/_boto3/test_s3_object.py b/tests/functional/streaming/_boto3/test_s3_object.py index b235bcff631..cd044abd0e7 100644 --- a/tests/functional/streaming/_boto3/test_s3_object.py +++ b/tests/functional/streaming/_boto3/test_s3_object.py @@ -12,7 +12,7 @@ from aws_lambda_powertools.utilities.streaming import S3Object from aws_lambda_powertools.utilities.streaming._s3_seekable_io import _S3SeekableIO from aws_lambda_powertools.utilities.streaming.compat import PowertoolsStreamingBody -from aws_lambda_powertools.utilities.streaming.transformations import GzipTransform +from aws_lambda_powertools.utilities.streaming.transformations import GzipTransform, ZipTransform @pytest.fixture @@ -91,3 +91,26 @@ def test_s3_readline_respects_size(s3_client, s3_client_stub, is_gzip, readline_ assert obj.tell() == len(payload) s3_client_stub.assert_no_pending_responses() + + +@pytest.mark.parametrize("is_gzip", [False, True], ids=["raw", "gzip"]) +def test_s3_closed_after_close_without_reading(s3_client, is_gzip): + # GIVEN an object that was never read nor inspected + obj = S3Object(bucket="bucket", key="key", boto3_client=s3_client, is_gzip=is_gzip) + + # WHEN it is closed + obj.close() + + # THEN it reports as closed, whether or not data transformations are configured + assert obj.closed is True + + +def test_s3_closed_does_not_make_requests_for_zip(s3_client, s3_client_stub): + # GIVEN an object with a zip transformation, which would read the archive as soon as it is created + obj = S3Object(bucket="bucket", key="key", boto3_client=s3_client) + obj.transform(ZipTransform(), in_place=True) + + # WHEN checking whether it is closed + # THEN no S3 request is issued (the stubber has no responses queued, so any call would fail) + assert obj.closed is False + s3_client_stub.assert_no_pending_responses()