Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions localstack-core/localstack/services/s3/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -1200,12 +1200,14 @@ def head_object(
response["ChecksumType"] = checksum_type

add_encryption_to_response(response, s3_object=s3_object)
object_tags = store.TAGS.tags.get(
get_unique_key_id(bucket_name, object_key, s3_object.version_id)
)
if object_tags:
response["TagCount"] = len(object_tags)

# if you specify the VersionId, AWS won't return the Expiration header, even if that's the current version
if not version_id and s3_bucket.lifecycle_rules:
object_tags = store.TAGS.tags.get(
get_unique_key_id(bucket_name, object_key, s3_object.version_id)
)
if expiration_header := self._get_expiration_header(
s3_bucket.lifecycle_rules,
bucket_name,
Expand Down
4 changes: 2 additions & 2 deletions tests/aws/services/s3/test_s3.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -2782,7 +2782,7 @@
}
},
"tests/aws/services/s3/test_s3.py::TestS3::test_head_object_fields": {
"recorded-date": "21-01-2025, 18:28:10",
"recorded-date": "27-11-2025, 15:29:30",
"recorded-content": {
"head-object": {
"AcceptRanges": "bytes",
Expand Down Expand Up @@ -10502,7 +10502,7 @@
}
},
"tests/aws/services/s3/test_s3.py::TestS3::test_get_object_part": {
"recorded-date": "07-07-2025, 17:56:14",
"recorded-date": "27-11-2025, 15:28:18",
"recorded-content": {
"multipart-upload": {
"Bucket": "<bucket:1>",
Expand Down
18 changes: 12 additions & 6 deletions tests/aws/services/s3/test_s3.validation.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,12 @@
"last_validated_date": "2025-01-21T18:27:10+00:00"
},
"tests/aws/services/s3/test_s3.py::TestS3::test_get_object_part": {
"last_validated_date": "2025-07-07T17:56:15+00:00",
"last_validated_date": "2025-11-27T15:28:19+00:00",
"durations_in_seconds": {
"setup": 1.4,
"call": 10.69,
"teardown": 1.07,
"total": 13.16
"setup": 0.99,
"call": 3.36,
"teardown": 0.98,
"total": 5.33
}
},
"tests/aws/services/s3/test_s3.py::TestS3::test_get_object_part_checksum[COMPOSITE]": {
Expand Down Expand Up @@ -198,7 +198,13 @@
"last_validated_date": "2025-01-21T18:31:25+00:00"
},
"tests/aws/services/s3/test_s3.py::TestS3::test_head_object_fields": {
"last_validated_date": "2025-01-21T18:28:10+00:00"
"last_validated_date": "2025-11-27T15:29:30+00:00",
"durations_in_seconds": {
"setup": 0.99,
"call": 0.44,
"teardown": 0.95,
"total": 2.38
}
},
"tests/aws/services/s3/test_s3.py::TestS3::test_invalid_range_error": {
"last_validated_date": "2025-01-21T18:27:45+00:00"
Expand Down
44 changes: 38 additions & 6 deletions tests/aws/services/s3/test_s3_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1897,12 +1897,6 @@ def test_object_tagging_versioned(self, s3_bucket, aws_client, snapshot):
snapshot.match("delete-object-tags-delete-marker-latest", e.value.response)

@markers.aws.validated
@markers.snapshot.skip_snapshot_verify(
paths=[
# TODO: support TagCount in HeadObject
"$.head-obj.TagCount",
]
)
def test_put_object_with_tags(self, s3_bucket, aws_client, snapshot):
object_key = "test-put-object-tagging"
# tagging must be a URL encoded string directly
Expand Down Expand Up @@ -1946,6 +1940,44 @@ def test_put_object_with_tags(self, s3_bucket, aws_client, snapshot):
get_object_tags = aws_client.s3.get_object_tagging(Bucket=s3_bucket, Key=object_key)
snapshot.match("get-object-tags-wrong-format-qs-2", get_object_tags)

@markers.aws.validated
def test_head_object_with_tags(self, s3_bucket, aws_client, snapshot):
object_key = "test-put-object-tagging"
# tagging must be a URL encoded string directly
tag_set = "tag1=tag1&tag2=tag2&tag="
put_object = aws_client.s3.put_object(
Bucket=s3_bucket, Key=object_key, Body="test-tagging", Tagging=tag_set
)
snapshot.match("put-object", put_object)

head_object = aws_client.s3.head_object(Bucket=s3_bucket, Key=object_key)
snapshot.match("head-obj-after-create", head_object)

tag_set_2 = {"TagSet": [{"Key": "tag3", "Value": "tag3"}]}
put_bucket_tags = aws_client.s3.put_object_tagging(
Bucket=s3_bucket, Key=object_key, Tagging=tag_set_2
)
snapshot.match("put-object-tags", put_bucket_tags)

head_object = aws_client.s3.head_object(Bucket=s3_bucket, Key=object_key)
snapshot.match("head-obj-after-tagging", head_object)

tag_set_2 = {
"TagSet": [
{"Key": "tag3", "Value": "tag3"},
{"Key": "tag4", "Value": "tag4"},
{"Key": "tag5", "Value": "tag5"},
]
}
aws_client.s3.put_object_tagging(Bucket=s3_bucket, Key=object_key, Tagging=tag_set_2)
head_object = aws_client.s3.head_object(Bucket=s3_bucket, Key=object_key)
snapshot.match("head-obj-after-overwrite", head_object)

aws_client.s3.put_object_tagging(Bucket=s3_bucket, Key=object_key, Tagging={"TagSet": []})

head_object = aws_client.s3.head_object(Bucket=s3_bucket, Key=object_key)
snapshot.match("head-obj-after-removal", head_object)

@markers.aws.validated
def test_object_tags_delete_or_overwrite_object(self, s3_bucket, aws_client, snapshot):
# verify that tags aren't kept after object deletion
Expand Down
78 changes: 77 additions & 1 deletion tests/aws/services/s3/test_s3_api.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -1442,7 +1442,7 @@
}
},
"tests/aws/services/s3/test_s3_api.py::TestS3BucketEncryption::test_s3_bucket_encryption_sse_s3": {
"recorded-date": "21-01-2025, 18:10:49",
"recorded-date": "27-11-2025, 15:27:11",
"recorded-content": {
"put-bucket-enc": {
"ResponseMetadata": {
Expand Down Expand Up @@ -5402,5 +5402,81 @@
}
}
}
},
"tests/aws/services/s3/test_s3_api.py::TestS3BucketObjectTagging::test_head_object_with_tags": {
"recorded-date": "27-11-2025, 15:35:45",
"recorded-content": {
"put-object": {
"ChecksumCRC32": "lpqTBg==",
"ChecksumType": "FULL_OBJECT",
"ETag": "\"b635a7fc30aa9091e0d236bee77e6844\"",
"ServerSideEncryption": "AES256",
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"head-obj-after-create": {
"AcceptRanges": "bytes",
"ContentLength": 12,
"ContentType": "binary/octet-stream",
"ETag": "\"b635a7fc30aa9091e0d236bee77e6844\"",
"LastModified": "datetime",
"Metadata": {},
"ServerSideEncryption": "AES256",
"TagCount": 3,
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"put-object-tags": {
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"head-obj-after-tagging": {
"AcceptRanges": "bytes",
"ContentLength": 12,
"ContentType": "binary/octet-stream",
"ETag": "\"b635a7fc30aa9091e0d236bee77e6844\"",
"LastModified": "datetime",
"Metadata": {},
"ServerSideEncryption": "AES256",
"TagCount": 1,
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"head-obj-after-overwrite": {
"AcceptRanges": "bytes",
"ContentLength": 12,
"ContentType": "binary/octet-stream",
"ETag": "\"b635a7fc30aa9091e0d236bee77e6844\"",
"LastModified": "datetime",
"Metadata": {},
"ServerSideEncryption": "AES256",
"TagCount": 3,
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"head-obj-after-removal": {
"AcceptRanges": "bytes",
"ContentLength": 12,
"ContentType": "binary/octet-stream",
"ETag": "\"b635a7fc30aa9091e0d236bee77e6844\"",
"LastModified": "datetime",
"Metadata": {},
"ServerSideEncryption": "AES256",
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
}
}
}
}
17 changes: 16 additions & 1 deletion tests/aws/services/s3/test_s3_api.validation.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@
}
},
"tests/aws/services/s3/test_s3_api.py::TestS3BucketEncryption::test_s3_bucket_encryption_sse_s3": {
"last_validated_date": "2025-01-21T18:10:49+00:00"
"last_validated_date": "2025-11-27T15:27:12+00:00",
"durations_in_seconds": {
"setup": 1.06,
"call": 0.67,
"teardown": 1.0,
"total": 2.73
}
},
"tests/aws/services/s3/test_s3_api.py::TestS3BucketEncryption::test_s3_default_bucket_encryption": {
"last_validated_date": "2025-01-21T18:10:45+00:00"
Expand Down Expand Up @@ -104,6 +110,15 @@
"total": 2.88
}
},
"tests/aws/services/s3/test_s3_api.py::TestS3BucketObjectTagging::test_head_object_with_tags": {
"last_validated_date": "2025-11-27T15:35:46+00:00",
"durations_in_seconds": {
"setup": 0.97,
"call": 1.08,
"teardown": 0.98,
"total": 3.03
}
},
"tests/aws/services/s3/test_s3_api.py::TestS3BucketObjectTagging::test_object_tagging_crud": {
"last_validated_date": "2025-11-27T14:45:28+00:00",
"durations_in_seconds": {
Expand Down
Loading