Repository: localstack/localstack

Issue:
Localstack doesn't return some metadata for S3 Range requests
Using s3 range request with localstack causes incorrect content type to be returned & some metadata to be missing:
To reproduce, put object:
`aws s3api put-object --endpoint http://localhost:3006 --bucket some-bucket --key some-dir/some-key --body text.txt --content-type text/plain --content-language en-US --cache-control no-cache`

And compare regular request vs range one:
`aws s3api get-object --endpoint http://localhost:3006 --bucket some-bucket --key some-dir/some-key output.txt --range bytes=0-3`
`aws s3api get-object --endpoint http://localhost:3006 --bucket some-bucket --key some-dir/some-key output.txt`

This works correctly (returns the same metadata regardless of using `--range bytes=0-3`) on aws s3.

Also:
Getting object with/without `range bytes=0-3` should return header "AcceptRanges: bytes" but it doesn't.


Discussion:

Resolving patch:
diff --git a/localstack/services/s3/s3_listener.py b/localstack/services/s3/s3_listener.py
--- a/localstack/services/s3/s3_listener.py
+++ b/localstack/services/s3/s3_listener.py
@@ -1,3 +1,4 @@
+import random
 import re
 import logging
 import json
@@ -306,6 +307,19 @@ def append_cors_headers(bucket_name, request_method, request_headers, response):
                     break
 
 
+def append_aws_request_troubleshooting_headers(response):
+    gen_amz_request_id = ''.join(random.choice('0123456789ABCDEF') for i in range(16))
+    if response.headers.get('x-amz-request-id') is None:
+        response.headers['x-amz-request-id'] = gen_amz_request_id
+    if response.headers.get('x-amz-id-2') is None:
+        response.headers['x-amz-id-2'] = 'MzRISOwyjmnup' + gen_amz_request_id + '7/JypPGXLh0OVFGcJaaO3KW/hRAqKOpIEEp'
+
+
+def add_accept_range_header(response):
+    if response.headers.get('accept-ranges') is None:
+        response.headers['accept-ranges'] = 'bytes'
+
+
 def append_last_modified_headers(response, content=None):
     """Add Last-Modified header with current time
     (if the response content is an XML containing <LastModified>, add that instead)"""
@@ -1021,6 +1035,7 @@ def return_response(self, method, path, data, headers, response):
             fix_metadata_key_underscores(response=response)
             fix_creation_date(method, path, response=response)
             fix_etag_for_multipart(data, headers, response)
+            append_aws_request_troubleshooting_headers(response)
 
             # Remove body from PUT response on presigned URL
             # https://github.com/localstack/localstack/issues/1317
@@ -1038,6 +1053,7 @@ def return_response(self, method, path, data, headers, response):
             # Honor response header overrides
             # https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html
             if method == 'GET':
+                add_accept_range_header(response)
                 query_map = urlparse.parse_qs(parsed.query, keep_blank_values=True)
                 for param_name, header_name in ALLOWED_HEADER_OVERRIDES.items():
                     if param_name in query_map:


Test patch:
diff --git a/tests/integration/test_s3.py b/tests/integration/test_s3.py
--- a/tests/integration/test_s3.py
+++ b/tests/integration/test_s3.py
@@ -303,6 +303,26 @@ def test_s3_get_response_content_type_same_as_upload_and_range(self):
         # clean up
         self._delete_bucket(bucket_name, [object_key])
 
+    def test_s3_get_get_object_headers(self):
+        object_key = 'sample.bin'
+        bucket_name = 'test-%s' % short_uid()
+        self.s3_client.create_bucket(Bucket=bucket_name)
+
+        chunk_size = 1024
+
+        with io.BytesIO() as data:
+            data.write(os.urandom(chunk_size * 2))
+            data.seek(0)
+            self.s3_client.upload_fileobj(data, bucket_name, object_key)
+
+        range_header = 'bytes=0-%s' % (chunk_size - 1)
+        resp = self.s3_client.get_object(Bucket=bucket_name, Key=object_key, Range=range_header)
+        self.assertEqual(resp.get('AcceptRanges'), 'bytes')
+        self.assertIn('x-amz-request-id', resp['ResponseMetadata']['HTTPHeaders'])
+        self.assertIn('x-amz-id-2', resp['ResponseMetadata']['HTTPHeaders'])
+        # clean up
+        self._delete_bucket(bucket_name, [object_key])
+
     def test_s3_head_response_content_length_same_as_upload(self):
         bucket_name = 'test-bucket-%s' % short_uid()
         self.s3_client.create_bucket(Bucket=bucket_name)