Let users know entity size in 416 responses

If a user sends a Range header with no satisfiable ranges, we send back
a 416 Requested Range Not Satisfiable response. Previously however,
there would be no indication of the size of the object they were
requesting, so they wouldn't know how to craft a satisfiable range. We
*do* send a Content-Length, but it is (correctly) the length of the
error message.

The RFC [1] has an answer for this:

>  A server generating a 416 (Range Not Satisfiable) response to a
>  byte-range request SHOULD send a Content-Range header field with an
>  unsatisfied-range value, as in the following example:
>
>    Content-Range: bytes */1234
>
>  The complete-length in a 416 response indicates the current length of
>  the selected representation.

Now, we'll send a Content-Range header for all 416 responses, including
those coming from the object server as well as those generated on a
proxy because of the Range mangling required to support EC policies.

[1] RFC 7233, section 4.2, although similar language was used in RFC
2616, sections 10.4.17 and 14.16

Change-Id: I80c7390fc6f84a10a212b0641bb07a64dfccbd45
This commit is contained in:
Tim Burke 2016-10-29 17:21:41 +02:00
parent a92836074c
commit e8a80e874a
8 changed files with 41 additions and 3 deletions

View File

@ -263,7 +263,12 @@ class GetContext(WSGIContext):
byteranges = req.range.ranges_for_length(
content_length_for_swob_range)
if not byteranges:
return HTTPRequestedRangeNotSatisfiable(request=req)
headers = {'Accept-Ranges': 'bytes'}
if have_complete_listing:
headers['Content-Range'] = 'bytes */%d' % (
content_length_for_swob_range, )
return HTTPRequestedRangeNotSatisfiable(
request=req, headers=headers)
first_byte, last_byte = byteranges[0]
# For some reason, swob.Range.ranges_for_length adds 1 to the
# last byte's position.

View File

@ -1250,6 +1250,8 @@ class Response(object):
if ranges == []:
self.status = 416
close_if_possible(app_iter)
self.headers['Content-Range'] = \
'bytes */%d' % self.content_length
# Setting body + app_iter to None makes us emit the default
# body text from RESPONSE_REASONS.
body = None

View File

@ -2310,6 +2310,9 @@ class ECObjectController(BaseObjectController):
resp.headers['Content-Length'] = resp.headers.get(
'X-Object-Sysmeta-Ec-Content-Length')
resp.fix_conditional_response()
if resp.status_int == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
resp.headers['Content-Range'] = 'bytes */%s' % resp.headers[
'X-Object-Sysmeta-Ec-Content-Length']
def _make_putter(self, node, part, req, headers):
return MIMEPutter.connect(

View File

@ -135,6 +135,7 @@ class TestDlo(Base):
self.assertRaises(ResponseError, file_item.read, size=7, offset=50)
self.assert_status(416)
self.assert_header('content-range', 'bytes */50')
def test_copy(self):
# Adding a new segment, copying the manifest, and then deleting the

View File

@ -1888,8 +1888,11 @@ class TestFile(Base):
# Otherwise, the byte-range-set is unsatisfiable.
self.assertRaises(ResponseError, file_item.read, hdrs=hdrs)
self.assert_status(416)
self.assert_header('content-range', 'bytes */%d' % file_length)
else:
self.assertEqual(file_item.read(hdrs=hdrs), data[-i:])
self.assert_header('content-range', 'bytes %d-%d/%d' % (
file_length - i, file_length - 1, file_length))
self.assert_header('etag', file_item.md5)
self.assert_header('accept-ranges', 'bytes')
@ -1903,6 +1906,7 @@ class TestFile(Base):
hdrs = {'Range': range_string}
self.assertRaises(ResponseError, file_item.read, hdrs=hdrs)
self.assert_status(416)
self.assert_header('content-range', 'bytes */%d' % file_length)
self.assert_header('etag', file_item.md5)
self.assert_header('accept-ranges', 'bytes')
@ -2067,6 +2071,7 @@ class TestFile(Base):
self.assertRaises(ResponseError, file_item.read, hdrs=hdrs)
self.assert_status(416)
self.assert_header('content-range', 'bytes */%d' % file_length)
def testRangedGetsWithLWSinHeader(self):
file_length = 10000

View File

@ -411,6 +411,12 @@ class TestDloGetManifest(DloTestCase):
headers={'Range': 'bytes=25-30'})
status, headers, body = self.call_dlo(req)
self.assertEqual(status, "416 Requested Range Not Satisfiable")
expected_headers = (
('Accept-Ranges', 'bytes'),
('Content-Range', 'bytes */25'),
)
for header_value_pair in expected_headers:
self.assertIn(header_value_pair, headers)
def test_get_range_many_segments_satisfiable(self):
req = swob.Request.blank('/v1/AUTH_test/mancon/manifest-many-segments',

View File

@ -1304,6 +1304,7 @@ class TestResponse(unittest.TestCase):
self.assertIn('The Range requested is not available', body)
self.assertEqual(resp.content_length, len(body))
self.assertEqual(resp.status, '416 Requested Range Not Satisfiable')
self.assertEqual(resp.content_range, 'bytes */10')
resp = swift.common.swob.Response(
body='1234567890', request=req,
@ -1321,6 +1322,7 @@ class TestResponse(unittest.TestCase):
body = ''.join(resp([], start_response))
self.assertEqual(body, '1234567890')
self.assertEqual(resp.status, '200 OK')
self.assertNotIn('Content-Range', resp.headers)
resp = swift.common.swob.Response(
body='1234567890', request=req,

View File

@ -6159,12 +6159,26 @@ class TestObjectECRangedGET(unittest.TestCase):
def test_unsatisfiable(self):
# Goes just one byte too far off the end of the object, so it's
# unsatisfiable
# unsatisfiable. This should be close enough that the object servers
# actually responded 206
obj_len = len(self.obj)
status, headers, _junk = self._get_obj(
"bytes=%d-%d" % (len(self.obj), len(self.obj) + 100))
"bytes=%d-%d" % (obj_len, obj_len + 100))
self.assertEqual(status, 416)
self.assertEqual(self.obj_etag, headers.get('Etag'))
self.assertEqual('bytes', headers.get('Accept-Ranges'))
self.assertIn('Content-Range', headers)
self.assertEqual('bytes */%d' % obj_len, headers['Content-Range'])
# Goes *way* too far off the end of the object, so we're looking at
# the (massaged) 416 from an object server
status, headers, _junk = self._get_obj(
"bytes=%d-" % (obj_len + 2 ** 30))
self.assertEqual(status, 416)
self.assertEqual(self.obj_etag, headers.get('Etag'))
self.assertEqual('bytes', headers.get('Accept-Ranges'))
self.assertIn('Content-Range', headers)
self.assertEqual('bytes */%d' % obj_len, headers['Content-Range'])
def test_off_end(self):
# Ranged GET that's mostly off the end of the object, but overlaps