diff --git a/moto/s3/exceptions.py b/moto/s3/exceptions.py
index 7ea21b096..3b33791c5 100644
--- a/moto/s3/exceptions.py
+++ b/moto/s3/exceptions.py
@@ -14,6 +14,11 @@ ERROR_WITH_CONDITION_NAME = """{% extends 'single_error' %}
{% block extra %}{{ condition }}{% endblock %}
"""
+ERROR_WITH_RANGE = """{% extends 'single_error' %}
+{% block extra %}{{ actual_size }}
+{{ range_requested }}{% endblock %}
+"""
+
class S3ClientError(RESTError):
def __init__(self, *args, **kwargs):
@@ -404,3 +409,18 @@ class PreconditionFailed(S3ClientError):
condition=failed_condition,
**kwargs
)
+
+
+class InvalidRange(S3ClientError):
+ code = 416
+
+ def __init__(self, range_requested, actual_size, **kwargs):
+ kwargs.setdefault("template", "range_error")
+ self.templates["range_error"] = ERROR_WITH_RANGE
+ super(InvalidRange, self).__init__(
+ "InvalidRange",
+ "The requested range is not satisfiable",
+ range_requested=range_requested,
+ actual_size=actual_size,
+ **kwargs
+ )
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index 4cb366195..b01bed1fb 100644
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -37,6 +37,7 @@ from .exceptions import (
ObjectNotInActiveTierError,
NoSystemTags,
PreconditionFailed,
+ InvalidRange,
)
from .models import (
s3_backend,
@@ -936,11 +937,15 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
else:
return 400, response_headers, ""
if begin < 0 or end > last or begin > min(end, last):
- return 416, response_headers, ""
+ raise InvalidRange(
+ actual_size=str(length), range_requested=request.headers.get("range")
+ )
response_headers["content-range"] = "bytes {0}-{1}/{2}".format(
begin, end, length
)
- return 206, response_headers, response_content[begin : end + 1]
+ content = response_content[begin : end + 1]
+ response_headers["content-length"] = len(content)
+ return 206, response_headers, content
def key_or_control_response(self, request, full_url, headers):
# Key and Control are lumped in because splitting out the regex is too much of a pain :/
@@ -967,9 +972,12 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
status_code, response_headers, response_content = response
if status_code == 200 and "range" in request.headers:
- return self._handle_range_header(
- request, response_headers, response_content
- )
+ try:
+ return self._handle_range_header(
+ request, response_headers, response_content
+ )
+ except S3ClientError as s3error:
+ return s3error.code, {}, s3error.description
return status_code, response_headers, response_content
def _control_response(self, request, full_url, headers):
diff --git a/tests/test_s3/red.jpg b/tests/test_s3/red.jpg
new file mode 100644
index 000000000..6fb9aed7c
Binary files /dev/null and b/tests/test_s3/red.jpg differ
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
index b213a9a72..d8f08e9ef 100644
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -4889,3 +4889,37 @@ def test_presigned_put_url_with_custom_headers():
s3.delete_object(Bucket=bucket, Key=key)
s3.delete_bucket(Bucket=bucket)
+
+
+@mock_s3
+def test_request_partial_content_should_contain_content_length():
+ bucket = "bucket"
+ object_key = "key"
+ s3 = boto3.resource("s3")
+ s3.create_bucket(Bucket=bucket)
+ s3.Object(bucket, object_key).put(Body="some text")
+
+ file = s3.Object(bucket, object_key)
+ response = file.get(Range="bytes=0-1024")
+ response["ContentLength"].should.equal(9)
+
+
+@mock_s3
+def test_request_partial_content_should_contain_actual_content_length():
+ bucket = "bucket"
+ object_key = "key"
+ s3 = boto3.resource("s3")
+ s3.create_bucket(Bucket=bucket)
+ s3.Object(bucket, object_key).put(Body="some text")
+
+ file = s3.Object(bucket, object_key)
+ requested_range = "bytes=1024-"
+ try:
+ file.get(Range=requested_range)
+ except botocore.client.ClientError as e:
+ e.response["Error"]["Code"].should.equal("InvalidRange")
+ e.response["Error"]["Message"].should.equal(
+ "The requested range is not satisfiable"
+ )
+ e.response["Error"]["ActualObjectSize"].should.equal("9")
+ e.response["Error"]["RangeRequested"].should.equal(requested_range)