Repository: getmoto/moto

Issue:
 S3 .get(Range=...) doesn't return ETag
# Summary

When specifying a `Range` parameter for an S3 `.get()`, the response doesn't include metadata like `ETag`.
(It works without the `Range` parameter.)

# Steps to reproduce

```
import boto3
from moto import mock_s3


with mock_s3():

    bucket = 'fake-bucket'
    key = 'my-file'

    boto3.client('s3').create_bucket(
        Bucket=bucket,
        CreateBucketConfiguration={
            'LocationConstraint': 'ap-southeast-2'
        },
    )

    obj = boto3.resource('s3').Object(bucket, key)

    obj.put(Body=b"1234")

    resp = obj.get()
    assert resp['ETag'] == obj.e_tag

    resp = obj.get(Range="bytes=0-1")
    assert resp['ETag'] == obj.e_tag

```

# Expected behavior

Response includes the following top-level keys:

* ResponseMetadata
* AcceptRanges
* LastModified
* ContentLength
* ETag
* ContentRange
* ContentType
* ServerSideEncryption
* Metadata
* Body



# actual behavior

Response includes the following top level keys:

* ResponseMetadata
* ContentLength
* ContentRange
* Metadata
* Body

So the final line in the script throws a KeyError.

```
Traceback (most recent call last):
  File "mwe.py", line 26, in <module>
    assert resp['ETag'] == obj.e_tag
KeyError: 'ETag'
```

# Installation

I `pip install`ed moto.

Python 3.8.10

boto                   2.49.0              
boto3                  1.18.26             
botocore               1.21.26             
moto                   2.2.3 
 S3 .get(Range=...) doesn't return ETag
# Summary

When specifying a `Range` parameter for an S3 `.get()`, the response doesn't include metadata like `ETag`.
(It works without the `Range` parameter.)

# Steps to reproduce

```
import boto3
from moto import mock_s3


with mock_s3():

    bucket = 'fake-bucket'
    key = 'my-file'

    boto3.client('s3').create_bucket(
        Bucket=bucket,
        CreateBucketConfiguration={
            'LocationConstraint': 'ap-southeast-2'
        },
    )

    obj = boto3.resource('s3').Object(bucket, key)

    obj.put(Body=b"1234")

    resp = obj.get()
    assert resp['ETag'] == obj.e_tag

    resp = obj.get(Range="bytes=0-1")
    assert resp['ETag'] == obj.e_tag

```

# Expected behavior

Response includes the following top-level keys:

* ResponseMetadata
* AcceptRanges
* LastModified
* ContentLength
* ETag
* ContentRange
* ContentType
* ServerSideEncryption
* Metadata
* Body



# actual behavior

Response includes the following top level keys:

* ResponseMetadata
* ContentLength
* ContentRange
* Metadata
* Body

So the final line in the script throws a KeyError.

```
Traceback (most recent call last):
  File "mwe.py", line 26, in <module>
    assert resp['ETag'] == obj.e_tag
KeyError: 'ETag'
```

# Installation

I `pip install`ed moto.

Python 3.8.10

boto                   2.49.0              
boto3                  1.18.26             
botocore               1.21.26             
moto                   2.2.3 


Discussion:
Thanks for raising this @mdavis-xyz - will mark it as an enhancement to return all attributes.
I see this issue as well.

For reference, we probably need to update function `_handle_range_header` in https://github.com/spulec/moto/blob/master/moto/s3/responses.py to start with `response_headers = headers`, so that all the other headers (including ETag) are passed through.
Thanks for raising this @mdavis-xyz - will mark it as an enhancement to return all attributes.
I see this issue as well.

For reference, we probably need to update function `_handle_range_header` in https://github.com/spulec/moto/blob/master/moto/s3/responses.py to start with `response_headers = headers`, so that all the other headers (including ETag) are passed through.

Resolving patch:
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -984,8 +984,7 @@ def _bucket_response_delete_keys(self, request, body, bucket_name):
             template.render(deleted=deleted_objects, delete_errors=error_names),
         )
 
-    def _handle_range_header(self, request, headers, response_content):
-        response_headers = {}
+    def _handle_range_header(self, request, response_headers, response_content):
         length = len(response_content)
         last = length - 1
         _, rspec = request.headers.get("range").split("=")


Test patch:
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -6486,3 +6486,24 @@ def test_head_object_should_return_default_content_type():
     s3.Object("testbucket", "testobject").content_type.should.equal(
         "binary/octet-stream"
     )
+
+
+@mock_s3
+def test_request_partial_content_should_contain_all_metadata():
+    # github.com/spulec/moto/issues/4203
+    bucket = "bucket"
+    object_key = "key"
+    body = "some text"
+    query_range = "0-3"
+
+    s3 = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)
+    s3.create_bucket(Bucket=bucket)
+    obj = boto3.resource("s3").Object(bucket, object_key)
+    obj.put(Body=body)
+
+    response = obj.get(Range="bytes={}".format(query_range))
+
+    assert response["ETag"] == obj.e_tag
+    assert response["LastModified"] == obj.last_modified
+    assert response["ContentLength"] == 4
+    assert response["ContentRange"] == "bytes {}/{}".format(query_range, len(body))